diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/tf/bot | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/server/tf/bot')
166 files changed, 30931 insertions, 0 deletions
diff --git a/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.cpp b/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.cpp new file mode 100644 index 0000000..721bd35 --- /dev/null +++ b/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.cpp @@ -0,0 +1,310 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_prepare_stickybomb_trap.cpp +// Place stickybombs to create a deadly trap +// Michael Booth, July 2010 + +#include "cbase.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h" +#include "tf_weapon_pipebomblauncher.h" + +#define MAX_STICKYBOMB_COUNT 8 + +ConVar tf_bot_stickybomb_density( "tf_bot_stickybomb_density", "0.0001", FCVAR_CHEAT, "Number of stickies to place per square inch" ); + + +//--------------------------------------------------------------------------------------------- +class PlaceStickyBombReply : public INextBotReply +{ +public: + virtual void OnSuccess( INextBot *bot ) // invoked when process completed successfully + { + CTFBot *me = ToTFBot( bot->GetEntity() ); + + CTFWeaponBase *myCurrentWeapon = me->m_Shared.GetActiveTFWeapon(); + if ( myCurrentWeapon && myCurrentWeapon->GetWeaponID() == TF_WEAPON_PIPEBOMBLAUNCHER ) + { + // launch the sticky + me->PressFireButton( 0.1f ); + + // increase the bomb count for this target area + if ( m_bombTargetArea ) + { + m_bombTargetArea->m_count++; + } + + if( m_pLaunchWaitTimer ) + { + // release the latch + m_pLaunchWaitTimer->Start( 0.15f ); + } + } + } + + virtual void OnFail( INextBot *bot, FailureReason reason )// invoked when process failed + { + // retry aim immediately + m_pLaunchWaitTimer->Invalidate(); + } + + void ClearData() + { + // Be sure to clear all members here, as we can potentially get an OnSuccess() call + // after the ~CTFBotPrepareStickybombTrap. + m_bombTargetArea = NULL; + m_pLaunchWaitTimer = NULL; + } + + CTFBotPrepareStickybombTrap::BombTargetArea *m_bombTargetArea; + CountdownTimer *m_pLaunchWaitTimer; +}; + + +static PlaceStickyBombReply bombReply; + + +//--------------------------------------------------------------------------------------------- +CTFBotPrepareStickybombTrap::CTFBotPrepareStickybombTrap( void ) +{ + m_myArea = NULL; +} + + +//--------------------------------------------------------------------------------------------- +CTFBotPrepareStickybombTrap::~CTFBotPrepareStickybombTrap( ) +{ + bombReply.ClearData(); +} + + +//--------------------------------------------------------------------------------------------- +// Return true if this Action has what it needs to perform right now +bool CTFBotPrepareStickybombTrap::IsPossible( CTFBot *me ) +{ + // don't lay a trap if we're in the midst of fighting + if ( /*me->IsInCombat() || */ me->GetTimeSinceLastInjury() < 1.0f ) + { + return false; + } + + if ( !me->IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + return false; + } + + CTFPipebombLauncher *stickyLauncher = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); + if ( stickyLauncher && !me->IsWeaponRestricted( stickyLauncher ) ) + { + if ( stickyLauncher->GetPipeBombCount() >= MAX_STICKYBOMB_COUNT || me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 ) + { + return false; + } + } + + return true; +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotPrepareStickybombTrap::InitBombTargetAreas( CTFBot *me ) +{ + const CUtlVector< CTFNavArea * > &invasionAreaVector = m_myArea->GetEnemyInvasionAreaVector( me->GetTeamNumber() ); + + // randomly shuffle the target areas + CUtlVector< CTFNavArea * > shuffleVector; + shuffleVector = invasionAreaVector; + int n = shuffleVector.Count(); + while( n > 1 ) + { + int k = RandomInt( 0, n-1 ); + n--; + + CTFNavArea *tmp = shuffleVector[n]; + shuffleVector[n] = shuffleVector[k]; + shuffleVector[k] = tmp; + } + + // initialize each target area to zero sticky bombs + m_bombTargetAreaVector.RemoveAll(); + + for( int i=0; i<shuffleVector.Count(); ++i ) + { + BombTargetArea target; + target.m_area = shuffleVector[i]; + target.m_count = 0; + + m_bombTargetAreaVector.AddToTail( target ); + } + + m_launchWaitTimer.Invalidate(); + + // Clean up any in-flight AimHeadTowards() replies, since changing m_bombTargetAreaVector + // might move memory and invalidate the current reply pointer. + me->GetBodyInterface()->ClearPendingAimReply(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPrepareStickybombTrap::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + // detonate old set of stickies + // me->PressAltFireButton(); + + // reload entire clip before laying sticky trap + CTFPipebombLauncher *stickyLauncher = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); + if ( stickyLauncher ) + { + m_isFullReloadNeeded = ( me->GetAmmoCount( TF_AMMO_SECONDARY ) >= stickyLauncher->GetMaxClip1() && stickyLauncher->Clip1() < stickyLauncher->GetMaxClip1() ); + } + else + { + m_isFullReloadNeeded = false; + } + + m_myArea = me->GetLastKnownArea(); + if ( !m_myArea ) + { + return Done( "No nav mesh" ); + } + + InitBombTargetAreas( me ); + + // own our view updating so we can aim + me->StopLookingAroundForEnemies(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPrepareStickybombTrap::Update( CTFBot *me, float interval ) +{ + if ( !TFGameRules()->InSetup() ) + { + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat ) + { + const float giveUpRange = 500.0f; + if ( me->IsDistanceBetweenLessThan( threat->GetLastKnownPosition(), giveUpRange ) ) + { + return Done( "Enemy nearby - giving up" ); + } + } + } + + if ( me->GetLastKnownArea() && me->GetLastKnownArea() != m_myArea ) + { + // we've moved + m_myArea = me->GetLastKnownArea(); + InitBombTargetAreas( me ); + } + + CTFWeaponBase *myCurrentWeapon = me->m_Shared.GetActiveTFWeapon(); + CTFPipebombLauncher *stickyLauncher = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); + + if ( !myCurrentWeapon || !stickyLauncher ) + { + return Done( "Missing weapon" ); + } + + if ( myCurrentWeapon->GetWeaponID() != TF_WEAPON_PIPEBOMBLAUNCHER ) + { + me->Weapon_Switch( stickyLauncher ); + } + + // reload fully + if ( m_isFullReloadNeeded ) + { + int maxClip = MIN( stickyLauncher->GetMaxClip1(), me->GetAmmoCount( TF_AMMO_SECONDARY ) ); + + if ( stickyLauncher->Clip1() >= maxClip ) + { + // fully reloaded + m_isFullReloadNeeded = false; + } + + me->PressReloadButton(); + + return Continue(); + } + + + if ( stickyLauncher->GetPipeBombCount() >= MAX_STICKYBOMB_COUNT || me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 ) + { + return Done( "Max sticky bombs reached" ); + } + + + // aim towards areas where enemy will come from + if ( m_launchWaitTimer.IsElapsed() ) + { + // find next target that needs bombs + int i; + for( i=0; i<m_bombTargetAreaVector.Count(); ++i ) + { + CTFNavArea *targetArea = m_bombTargetAreaVector[i].m_area; + + int desiredCount = tf_bot_stickybomb_density.GetFloat() * targetArea->GetSizeX() * targetArea->GetSizeY(); + if ( desiredCount < 1 ) + { + desiredCount = 1; + } + + if ( m_bombTargetAreaVector[i].m_count < desiredCount ) + { + // place a sticky on this area + bombReply.m_bombTargetArea = &m_bombTargetAreaVector[i]; + + // this timer causes us to wait until the aim finishes and launched before we start another aim + m_launchWaitTimer.Start( 2.0f ); + bombReply.m_pLaunchWaitTimer = &m_launchWaitTimer; + + Vector bombSpot = targetArea->GetRandomPoint(); + + me->GetBodyInterface()->AimHeadTowards( bombSpot, IBody::IMPORTANT, 5.0f, &bombReply, "Aiming a sticky bomb" ); + + break; + } + } + + if ( i == m_bombTargetAreaVector.Count() ) + { + return Done( "Exhausted bomb target areas" ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotPrepareStickybombTrap::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + // clean up any in-flight AimHeadTowards() replies + me->GetBodyInterface()->ClearPendingAimReply(); + + me->StartLookingAroundForEnemies(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPrepareStickybombTrap::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + // this behavior is transitory - if we need to do something else, just give up + return Done(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPrepareStickybombTrap::OnInjured( CTFBot *me, const CTakeDamageInfo &info ) +{ + return TryDone( RESULT_IMPORTANT, "Ouch!" ); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotPrepareStickybombTrap::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_NO; +} diff --git a/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h b/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h new file mode 100644 index 0000000..0abded3 --- /dev/null +++ b/game/server/tf/bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_prepare_stickybomb_trap.h +// Place stickybombs to create a deadly trap +// Michael Booth, July 2010 + +#ifndef TF_BOT_PREPARE_STICKYBOMB_TRAP_H +#define TF_BOT_PREPARE_STICKYBOMB_TRAP_H + +class CTFBotPrepareStickybombTrap : public Action< CTFBot > +{ +public: + CTFBotPrepareStickybombTrap( void ); + virtual ~CTFBotPrepareStickybombTrap( ); + + static bool IsPossible( CTFBot *me ); // Return true if this Action has what it needs to perform right now + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual ActionResult< CTFBot > OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "PrepareStickybombTrap"; }; + + struct BombTargetArea + { + CTFNavArea *m_area; + int m_count; + }; + +private: + bool m_isFullReloadNeeded; + + CTFNavArea *m_myArea; + + CUtlVector< BombTargetArea > m_bombTargetAreaVector; + void InitBombTargetAreas( CTFBot *me ); + CountdownTimer m_launchWaitTimer; +}; + +#endif // TF_BOT_PREPARE_STICKYBOMB_TRAP_H diff --git a/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.cpp b/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.cpp new file mode 100644 index 0000000..d788892 --- /dev/null +++ b/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.cpp @@ -0,0 +1,342 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_stickybomb_sentrygun.cpp +// Destroy the given sentrygun with stickybombs +// Michael Booth, August 2010 + +#include "cbase.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/demoman/tf_bot_stickybomb_sentrygun.h" +#include "tf_weapon_pipebomblauncher.h" +#include "tf_obj_sentrygun.h" +#include "NextBotUtil.h" + +ConVar tf_bot_sticky_base_range( "tf_bot_sticky_base_range", "800", FCVAR_CHEAT ); +ConVar tf_bot_sticky_charge_rate( "tf_bot_sticky_charge_rate", "0.01", FCVAR_CHEAT, "Seconds of charge per unit range beyond base" ); + + +//--------------------------------------------------------------------------------------------- +CTFBotStickybombSentrygun::CTFBotStickybombSentrygun( CObjectSentrygun *sentrygun ) +{ + m_sentrygun = sentrygun; + m_hasGivenAim = false; +} + + +//--------------------------------------------------------------------------------------------- +CTFBotStickybombSentrygun::CTFBotStickybombSentrygun( CObjectSentrygun *sentrygun, float aimYaw, float aimPitch, float aimCharge ) +{ + m_sentrygun = sentrygun; + m_hasGivenAim = true; + m_givenYaw = aimYaw; + m_givenPitch = aimPitch; + m_givenCharge = aimCharge; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotStickybombSentrygun::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + // detonate old set of stickies + me->PressAltFireButton(); + + // own our view updating so we can aim + me->StopLookingAroundForEnemies(); + + m_isFullReloadNeeded = true; + + // STOP + me->SetAbsVelocity( vec3_origin ); + + m_searchPitch = 0.0f; + m_hasTarget = false; + m_searchTimer.Start( 3.0f ); + + m_isChargingShot = false; + + if ( m_hasGivenAim ) + { + m_hasTarget = true; + + // remember where we are standing - if we move for any reason, we'll need to re-search + m_launchSpot = me->GetAbsOrigin(); + + // start charging up the sticky launch + m_chargeToLaunch = m_givenCharge; + m_isChargingShot = true; + + // aim along given pitch/yaw + QAngle angles; + angles.x = m_givenPitch; + angles.y = m_givenYaw; + angles.z = 0.0f; + + Vector aimForward; + AngleVectors( angles, &aimForward ); + + m_eyeAimTarget = me->EyePosition() + 1500.0f * aimForward; + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBotStickybombSentrygun::IsAimOnTarget( CTFBot *me, float pitch, float yaw, float charge ) +{ + // estimate impact spot + Vector impactSpot = me->EstimateStickybombProjectileImpactPosition( pitch, yaw, charge ); + + // check if impactSpot landed near sentry + const float explosionRadius = 75.0f; + if ( ( m_sentrygun->WorldSpaceCenter() - impactSpot ).IsLengthLessThan( explosionRadius ) ) + { + trace_t trace; + NextBotTraceFilterIgnoreActors filter( NULL, COLLISION_GROUP_NONE ); + + UTIL_TraceLine( m_sentrygun->WorldSpaceCenter(), impactSpot, MASK_SOLID_BRUSHONLY, &filter, &trace ); + if ( !trace.DidHit() ) + { + // NDebugOverlay::Cross3D( impactSpot, 10.0f, 100, 255, 0, true, 60.0f ); + return true; + } + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotStickybombSentrygun::Update( CTFBot *me, float interval ) +{ + CTFWeaponBase *myCurrentWeapon = me->m_Shared.GetActiveTFWeapon(); + CTFPipebombLauncher *stickyLauncher = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); + + if ( !myCurrentWeapon || !stickyLauncher ) + { + return Done( "Missing weapon" ); + } + + if ( myCurrentWeapon->GetWeaponID() != TF_WEAPON_PIPEBOMBLAUNCHER ) + { + me->Weapon_Switch( stickyLauncher ); + } + + if ( m_sentrygun == NULL || !m_sentrygun->IsAlive() ) + { + return Done( "Sentry destroyed" ); + } + + if ( !m_hasTarget && m_searchTimer.IsElapsed() ) + { + return Done( "Can't find aim" ); + } + + // reload fully + if ( m_isFullReloadNeeded ) + { + int maxClip = MIN( stickyLauncher->GetMaxClip1(), me->GetAmmoCount( TF_AMMO_SECONDARY ) ); + + if ( stickyLauncher->Clip1() >= maxClip ) + { + // fully reloaded + m_isFullReloadNeeded = false; + } + + me->PressReloadButton(); + + return Continue(); + } + + int requiredStickyBombs = 3; + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + // launch more stickies to make sure we take out beefed-up sentries + requiredStickyBombs = 5; + } + + if ( stickyLauncher->GetPipeBombCount() >= requiredStickyBombs || me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 ) + { + // stickies laid - detonate them once they are on the ground + const CUtlVector< CHandle< CTFGrenadePipebombProjectile > > &pipeVector = stickyLauncher->GetPipeBombVector(); + + int i; + for( i=0; i<pipeVector.Count(); ++i ) + { + if ( pipeVector[i].Get() && !pipeVector[i]->m_bTouched ) + { + break; + } + } + + if ( i == pipeVector.Count() ) + { + // stickies are on the ground + me->PressAltFireButton(); + + if ( me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 ) + { + return Done( "Out of ammo" ); + } + } + } + else if ( m_isChargingShot ) + { + // fudge charge time a bit longer - better to overshoot + float stickyChargeTime = 1.1f * m_chargeToLaunch * TF_PIPEBOMB_MAX_CHARGE_TIME; + + me->GetBodyInterface()->AimHeadTowards( m_eyeAimTarget, IBody::CRITICAL, 0.3f, NULL, "Aiming a sticky bomb at a sentrygun" ); + + if ( gpGlobals->curtime - stickyLauncher->GetChargeBeginTime() >= stickyChargeTime ) + { + // let go + me->ReleaseFireButton(); + m_isChargingShot = false; + } + else + { + me->PressFireButton(); + } + } + else if ( stickyLauncher->m_flNextPrimaryAttack < gpGlobals->curtime ) + { + // if we've moved, we need to re-search + if ( m_hasTarget ) + { + const float tolerance = 1.0f; + if ( me->IsRangeGreaterThan( m_launchSpot, tolerance ) ) + { + m_hasTarget = false; + m_searchTimer.Reset(); + } + } + + if ( !m_hasTarget ) + { + // search for angle to land sticky near sentry + Vector toSentry = m_sentrygun->WorldSpaceCenter() - me->EyePosition(); + + QAngle angles; + VectorAngles( toSentry, angles ); + + float bestYaw = 0.0f; + float bestPitch = 0.0f; + float bestCharge = 1.0f; + + const int trials = 100; + for( int t=0; t<trials; ++t ) + { + float yaw = angles.y + RandomFloat( -30.0f, 30.0f ); + // float pitch = ( trials & 0x1 ) ? m_searchPitch : -m_searchPitch; + float pitch = RandomFloat( -85.0f, 85.0f ); + + float charge = 0.0f; + if ( toSentry.IsLengthGreaterThan( tf_bot_sticky_base_range.GetBool() ) ) + { + charge = RandomFloat( 0.1f, 1.0f ); + + // skew towards zero - full charge shots are seldom required + charge *= charge; + } + + if ( IsAimOnTarget( me, pitch, yaw, charge ) ) + { + // found target aim - keep one we find with least required + // charge, because we need to be fast in combat + if ( charge < bestCharge ) + { + m_hasTarget = true; + + bestCharge = charge; + m_chargeToLaunch = bestCharge; + + bestYaw = yaw; + bestPitch = pitch; + + if ( bestCharge < 0.01 ) + { + // as quick as possible - no need to search further + break; + } + } + } + } + + // aim along yaw/pitch to reach impact spot + angles.x = bestPitch; + angles.y = bestYaw; + angles.z = 0.0f; + + Vector aimForward; + AngleVectors( angles, &aimForward ); + + // always recompute eye aim target so we can update our view + m_eyeAimTarget = me->EyePosition() + 500.0f * aimForward; + me->GetBodyInterface()->AimHeadTowards( m_eyeAimTarget, IBody::CRITICAL, 0.3f, NULL, "Searching for aim..." ); + } + + if ( m_hasTarget ) + { + // remember where we are standing - if we move for any reason, we'll need to re-search + m_launchSpot = me->GetAbsOrigin(); + + // start charging up the sticky launch + me->PressFireButton(); + m_isChargingShot = true; + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotStickybombSentrygun::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + // detonate any stickes left out there + me->PressAltFireButton(); + + me->StartLookingAroundForEnemies(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotStickybombSentrygun::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + // detonate any stickes left out there + me->PressAltFireButton(); + + // this behavior is transitory - if we need to do something else, just give up + return Done(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotStickybombSentrygun::OnInjured( CTFBot *me, const CTakeDamageInfo &info ) +{ + return TryDone( RESULT_IMPORTANT, "Ouch!" ); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotStickybombSentrygun::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotStickybombSentrygun::ShouldHurry( const INextBot *me ) const +{ + // while killing a sentry we're "hurrying" so we don't dodge + return ANSWER_YES; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotStickybombSentrygun::ShouldRetreat( const INextBot *me ) const +{ + // stay stuck in to try to kill that gun! + return ANSWER_NO; +} diff --git a/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.h b/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.h new file mode 100644 index 0000000..901e541 --- /dev/null +++ b/game/server/tf/bot/behavior/demoman/tf_bot_stickybomb_sentrygun.h @@ -0,0 +1,51 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_stickybomb_sentrygun.h +// Destroy the given sentrygun with stickybombs +// Michael Booth, August 2010 + +#ifndef TF_BOT_STICKYBOMB_SENTRY_H +#define TF_BOT_STICKYBOMB_SENTRY_H + +class CObjectSentrygun; + + +class CTFBotStickybombSentrygun : public Action< CTFBot > +{ +public: + CTFBotStickybombSentrygun( CObjectSentrygun *sentrygun ); + CTFBotStickybombSentrygun( CObjectSentrygun *sentrygun, float aimYaw, float aimPitch, float aimCharge ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual ActionResult< CTFBot > OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info ); + + virtual QueryResultType ShouldHurry( const INextBot *me ) const; + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + + virtual const char *GetName( void ) const { return "StickybombSentrygun"; }; + +private: + float m_givenYaw, m_givenPitch, m_givenCharge; + bool m_hasGivenAim; + + bool m_isFullReloadNeeded; + + CHandle< CObjectSentrygun > m_sentrygun; + + bool m_isChargingShot; + + CountdownTimer m_searchTimer; + bool m_hasTarget; + Vector m_eyeAimTarget; + Vector m_launchSpot; + float m_chargeToLaunch; + float m_searchPitch; + bool IsAimOnTarget( CTFBot *me, float pitch, float yaw, float charge ); +}; + +#endif // TF_BOT_STICKYBOMB_SENTRY_H diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.cpp b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.cpp new file mode 100644 index 0000000..c86318f --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.cpp @@ -0,0 +1,123 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// Michael Booth, September 2012 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_obj.h" +#include "tf_obj_sentrygun.h" +#include "tf_obj_dispenser.h" +#include "tf_gamerules.h" +#include "tf_weapon_builder.h" +#include "bot/tf_bot.h" +#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.h" +#include "bot/map_entities/tf_bot_hint_sentrygun.h" +#include "bot/map_entities/tf_bot_hint_teleporter_exit.h" +#include "string_t.h" +#include "tf_fx.h" + +extern ConVar tf_bot_engineer_mvm_building_health_multiplier; + +//--------------------------------------------------------------------------------------------- +CTFBotMvMEngineerBuildSentryGun::CTFBotMvMEngineerBuildSentryGun( CTFBotHintSentrygun* pSentryHint ) +{ + m_sentryBuildHint = pSentryHint; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMvMEngineerBuildSentryGun::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + me->StartBuildingObjectOfType( OBJ_SENTRYGUN ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMvMEngineerBuildSentryGun::Update( CTFBot *me, float interval ) +{ + if ( m_sentryBuildHint == NULL ) + return Done( "No hint entity" ); + + float rangeToBuildSpot = me->GetRangeTo( m_sentryBuildHint->GetAbsOrigin() ); + + if ( rangeToBuildSpot < 200.0f ) + { + // crouch as we get close so we don't overshoot + me->PressCrouchButton(); + + me->GetBodyInterface()->AimHeadTowards( m_sentryBuildHint->GetAbsOrigin(), IBody::MANDATORY, 0.1f, NULL, "Placing sentry" ); + } + + // various interruptions could mean we're away from our build location - move to it + if ( rangeToBuildSpot > 25.0f ) + { + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_path.Compute( me, m_sentryBuildHint->GetAbsOrigin(), cost ); + } + + m_path.Update( me ); + + if ( !m_path.IsValid() ) + { + return Done( "Path failed" ); + } + + return Continue(); + } + + if ( !m_delayBuildTime.HasStarted() ) + { + m_delayBuildTime.Start( 0.1f ); + TFGameRules()->PushAllPlayersAway( m_sentryBuildHint->GetAbsOrigin(), 400, 500, TF_TEAM_RED ); + } + else if ( m_delayBuildTime.HasStarted() && m_delayBuildTime.IsElapsed() ) + { + // destroy previous object + me->DetonateObjectOfType( OBJ_SENTRYGUN, MODE_SENTRYGUN_NORMAL, true ); + + // directly create a sentry gun at the precise position and orientation desired + m_sentry = (CObjectSentrygun *)CreateEntityByName( "obj_sentrygun" ); + if ( m_sentry ) + { + m_sentry->SetName( m_sentryBuildHint->GetEntityName() ); + + m_sentryBuildHint->IncrementUseCount(); + m_sentry->m_nDefaultUpgradeLevel = 2; + + m_sentry->SetAbsOrigin( m_sentryBuildHint->GetAbsOrigin() ); + m_sentry->SetAbsAngles( QAngle( 0, m_sentryBuildHint->GetAbsAngles().y, 0 ) ); + m_sentry->Spawn(); + + m_sentry->StartPlacement( me ); + m_sentry->StartBuilding( me ); + + // the sentry owns this hint now + m_sentryBuildHint->SetOwnerEntity( m_sentry ); + + m_sentry = NULL; + } + + return Done( "Built a sentry" ); + } + + return Continue(); +} + + + +//--------------------------------------------------------------------------------------------- +void CTFBotMvMEngineerBuildSentryGun::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + if ( m_sentry.Get() ) + { + m_sentry->DropCarriedObject( me ); + UTIL_Remove( m_sentry ); + m_sentry = NULL; + } +} diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.h b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.h new file mode 100644 index 0000000..199492f --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.h @@ -0,0 +1,29 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// Michael Booth, September 2012 + +#ifndef TF_BOT_MVM_ENGINEER_BUILD_SENTRYGUN_H +#define TF_BOT_MVM_ENGINEER_BUILD_SENTRYGUN_H + +class CTFBotHintSentrygun; + +class CTFBotMvMEngineerBuildSentryGun : public Action< CTFBot > +{ +public: + CTFBotMvMEngineerBuildSentryGun( CTFBotHintSentrygun* pSentryHint ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual const char *GetName( void ) const { return "MvMEngineerBuildSentryGun"; }; + +private: + CHandle< CTFBotHintSentrygun > m_sentryBuildHint; + CHandle< CObjectSentrygun > m_sentry; + + CountdownTimer m_delayBuildTime; + CountdownTimer m_repathTimer; + PathFollower m_path; +}; + +#endif // TF_BOT_MVM_ENGINEER_BUILD_SENTRYGUN_H diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.cpp b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.cpp new file mode 100644 index 0000000..024b981 --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.cpp @@ -0,0 +1,105 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// Michael Booth, September 2012 + +#include "cbase.h" +#include "string_t.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_obj.h" +#include "tf_obj_sentrygun.h" +#include "tf_obj_dispenser.h" +#include "tf_gamerules.h" +#include "tf_weapon_builder.h" +#include "bot/tf_bot.h" +#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.h" +#include "bot/map_entities/tf_bot_hint_teleporter_exit.h" + +ConVar tf_bot_engineer_mvm_building_health_multiplier( "tf_bot_engineer_building_health_multiplier", "2", FCVAR_CHEAT ); + +extern ConVar tf_bot_path_lookahead_range; + + +//--------------------------------------------------------------------------------------------- +CTFBotMvMEngineerBuildTeleportExit::CTFBotMvMEngineerBuildTeleportExit( CTFBotHintTeleporterExit *hint ) +{ + m_teleporterBuildHint = hint; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMvMEngineerBuildTeleportExit::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMvMEngineerBuildTeleportExit::Update( CTFBot *me, float interval ) +{ + if ( m_teleporterBuildHint == NULL ) + return Done( "No hint entity" ); + + // various interruptions could mean we're away from our build location - move to it + if ( me->IsRangeGreaterThan( m_teleporterBuildHint->GetAbsOrigin(), 25.0f ) ) + { + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, m_teleporterBuildHint->GetAbsOrigin(), cost ); + } + + m_path.Update( me ); + + if ( !m_path.IsValid() ) + { + return Done( "Path failed" ); + } + + return Continue(); + } + + if ( !m_delayBuildTime.HasStarted() ) + { + m_delayBuildTime.Start( 0.1f ); + TFGameRules()->PushAllPlayersAway( m_teleporterBuildHint->GetAbsOrigin(), 400, 500, TF_TEAM_RED ); + } + else if ( m_delayBuildTime.IsElapsed() ) + { + // destroy previous object + me->DetonateObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT, true ); + + // directly create at the precise position and orientation desired + CObjectTeleporter* myTeleporter = (CObjectTeleporter *)CreateEntityByName( "obj_teleporter" ); + if ( myTeleporter ) + { + myTeleporter->SetAbsOrigin( m_teleporterBuildHint->GetAbsOrigin() ); + myTeleporter->SetAbsAngles( QAngle( 0, m_teleporterBuildHint->GetAbsAngles().y, 0 ) ); + myTeleporter->SetObjectMode( MODE_TELEPORTER_EXIT ); + myTeleporter->Spawn(); + + myTeleporter->SetTeleportWhere( me->GetTeleportWhere() ); + + if ( me->ShouldQuickBuild() ) + { + myTeleporter->ForceQuickBuild(); + } + + myTeleporter->StartPlacement( me ); + myTeleporter->StartBuilding( me ); + + int iHealth = myTeleporter->GetMaxHealthForCurrentLevel() * tf_bot_engineer_mvm_building_health_multiplier.GetFloat(); + myTeleporter->SetMaxHealth( iHealth ); + myTeleporter->SetHealth( iHealth ); + + m_teleporterBuildHint->SetOwnerEntity( myTeleporter ); + + me->EmitSound( "Engineer.MVM_AutoBuildingTeleporter02" ); + + return Done( "Teleport exit built" ); + } + } + + return Continue(); +} diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.h b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.h new file mode 100644 index 0000000..7acb0b6 --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.h @@ -0,0 +1,27 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// Michael Booth, September 2012 + +#ifndef TF_BOT_MVM_ENGINEER_BUILD_TELEPORTER_H +#define TF_BOT_MVM_ENGINEER_BUILD_TELEPORTER_H + +class CTFBotHintTeleporterExit; + +class CTFBotMvMEngineerBuildTeleportExit : public Action< CTFBot > +{ +public: + CTFBotMvMEngineerBuildTeleportExit( CTFBotHintTeleporterExit *hint ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "MvMEngineerBuildTeleportExit"; }; + +private: + CHandle< CTFBotHintTeleporterExit > m_teleporterBuildHint; + + CountdownTimer m_delayBuildTime; + CountdownTimer m_repathTimer; + PathFollower m_path; +}; + +#endif // TF_BOT_MVM_ENGINEER_BUILD_TELEPORTER_H diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.cpp b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.cpp new file mode 100644 index 0000000..c6157dc --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.cpp @@ -0,0 +1,662 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// Michael Booth, September 2012 + +#include "cbase.h" +#include "nav_mesh/tf_nav_mesh.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_obj_sentrygun.h" +#include "tf_obj_teleporter.h" +#include "bot/tf_bot.h" +#include "bot/map_entities/tf_bot_hint_engineer_nest.h" +#include "bot/map_entities/tf_bot_hint_sentrygun.h" +#include "bot/map_entities/tf_bot_hint_teleporter_exit.h" + +#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h" +#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_sentry.h" +#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_build_teleporter.h" +#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.h" +#include "bot/behavior/tf_bot_retreat_to_cover.h" + + +ConVar tf_bot_engineer_mvm_sentry_hint_bomb_forward_range( "tf_bot_engineer_mvm_sentry_hint_bomb_forward_range", "0", FCVAR_CHEAT ); +ConVar tf_bot_engineer_mvm_sentry_hint_bomb_backward_range( "tf_bot_engineer_mvm_sentry_hint_bomb_backward_range", "3000", FCVAR_CHEAT ); +ConVar tf_bot_engineer_mvm_hint_min_distance_from_bomb( "tf_bot_engineer_mvm_hint_min_distance_from_bomb", "1300", FCVAR_CHEAT ); + +struct BombInfo_t +{ + Vector m_vPosition; + float m_flMinBattleFront; + float m_flMaxBattleFront; +}; + + +bool GetBombInfo( BombInfo_t* pBombInfo = NULL ) +{ + // find the incursion distance of the current "front" (the location of the bomb) + + // first find farthest bomb delivery distance of invading team since maps + // have different spawn room sizes and geometries + float battlefront = 0.0f; + + for( int n=0; n<TheNavAreas.Count(); ++n ) + { + CTFNavArea *area = (CTFNavArea *)TheNavAreas[n]; + + if ( area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) ) + { + continue; + } + + float areaDistanceToTarget = area->GetTravelDistanceToBombTarget(); + if ( areaDistanceToTarget > battlefront && areaDistanceToTarget > 0.0f ) + { + battlefront = areaDistanceToTarget; + } + } + + + // find the travel distance from the bomb to the delivery target and use it as the front + CCaptureFlag *flag = NULL; + Vector vBombSpot(0, 0, 0); + for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i ) + { + CCaptureFlag *pTempFlag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[i] ); + Vector vTempBombSpot; + CTFPlayer *carrier = ToTFPlayer( pTempFlag->GetOwnerEntity() ); + if ( carrier ) + { + vTempBombSpot = carrier->GetAbsOrigin(); + } + else + { + vTempBombSpot = pTempFlag->WorldSpaceCenter(); + } + + CTFNavArea *flagArea = (CTFNavArea *)TheNavMesh->GetNearestNavArea( vTempBombSpot, false, 1000.0f ); + if ( flagArea ) + { + float flagDistanceToTarget = flagArea->GetTravelDistanceToBombTarget(); + + if ( flagDistanceToTarget < battlefront && flagDistanceToTarget >= 0.0f ) + { + battlefront = flagDistanceToTarget; + flag = pTempFlag; + vBombSpot = vTempBombSpot; + } + } + } + + float flMaxBattlefront = battlefront + tf_bot_engineer_mvm_sentry_hint_bomb_backward_range.GetFloat(); + float flMinBattlefront = battlefront - tf_bot_engineer_mvm_sentry_hint_bomb_forward_range.GetFloat(); + + if ( pBombInfo ) + { + pBombInfo->m_vPosition = vBombSpot; + pBombInfo->m_flMinBattleFront = flMinBattlefront; + pBombInfo->m_flMaxBattleFront = flMaxBattlefront; + } + + return flag ? true : false; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMvMEngineerIdle::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + me->StopLookingAroundForEnemies(); + + m_sentryHint = NULL; + m_teleporterHint = NULL; + m_nestHint = NULL; + m_nTeleportedCount = 0; + m_bTeleportedToHint = false; + m_bTriedToDetonateStaleNest = false; + + return Continue(); +} + + +void CTFBotMvMEngineerIdle::TakeOverStaleNest( CBaseTFBotHintEntity* pHint, CTFBot *me ) +{ + if ( pHint != NULL && pHint->OwnerObjectHasNoOwner() ) + { + CBaseObject* pObj = static_cast< CBaseObject* >( pHint->GetOwnerEntity() ); + pObj->SetOwnerEntity( me ); + pObj->SetBuilder( me ); + me->AddObject( pObj ); + } +} + + +bool CTFBotMvMEngineerIdle::ShouldAdvanceNestSpot( CTFBot *me ) +{ + if ( !m_nestHint ) + { + return false; + } + + if ( !m_reevaluateNestTimer.HasStarted() ) + { + m_reevaluateNestTimer.Start( 5.f ); + return false; + } + + for ( int i=0; i<me->GetObjectCount(); ++i ) + { + CBaseObject *pObj = me->GetObject( i ); + if ( pObj && pObj->GetHealth() < pObj->GetMaxHealth() ) + { + // if the nest is under attack, don't advance the nest + m_reevaluateNestTimer.Start( 5.f ); + return false; + } + } + + if ( m_reevaluateNestTimer.IsElapsed() ) + { + m_reevaluateNestTimer.Invalidate(); + } + + BombInfo_t bombInfo; + if ( GetBombInfo( &bombInfo ) ) + { + if ( m_nestHint ) + { + CTFNavArea *hintArea = (CTFNavArea *)TheNavMesh->GetNearestNavArea( m_nestHint->GetAbsOrigin(), false, 1000.0f ); + if ( hintArea ) + { + float hintDistanceToTarget = hintArea->GetTravelDistanceToBombTarget(); + + bool bShouldAdvance = ( hintDistanceToTarget > bombInfo.m_flMaxBattleFront ); + + return bShouldAdvance; + } + } + } + + return false; +} + + +void CTFBotMvMEngineerIdle::TryToDetonateStaleNest() +{ + if ( m_bTriedToDetonateStaleNest ) + return; + + // wait until the engy finish building his nest + if ( ( m_sentryHint && !m_sentryHint->OwnerObjectFinishBuilding() ) || + ( m_teleporterHint && !m_teleporterHint->OwnerObjectFinishBuilding() ) ) + return; + + // collect all existing and active teleporter hints + CUtlVector< CTFBotHintEngineerNest* > activeEngineerNest; + for ( int i=0; i<ITFBotHintEntityAutoList::AutoList().Count(); ++i ) + { + CBaseTFBotHintEntity *pHint = static_cast<CBaseTFBotHintEntity*>( ITFBotHintEntityAutoList::AutoList()[i] ); + if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_ENGINEER_NEST ) && pHint->IsEnabled() && pHint->GetOwnerEntity() == NULL ) + { + activeEngineerNest.AddToTail( static_cast< CTFBotHintEngineerNest* >( pHint ) ); + } + } + + // try to detonate stale nest that's out of range, when engineer finished building his nest + for ( int i=0; i<activeEngineerNest.Count(); ++i ) + { + CTFBotHintEngineerNest *pNest = activeEngineerNest[i]; + if ( pNest->IsStaleNest() ) + { + pNest->DetonateStaleNest(); + } + } + + m_bTriedToDetonateStaleNest = true; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMvMEngineerIdle::Update( CTFBot *me, float interval ) +{ + if ( !me->IsAlive() ) + { + // don't do anything when I'm dead + return Done(); + } + + // Always equip my wrench + CBaseCombatWeapon *wrench = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE ); + if ( wrench ) + { + me->Weapon_Switch( wrench ); + } + + if ( m_nestHint == NULL || ShouldAdvanceNestSpot( me ) ) + { + if ( m_findHintTimer.HasStarted() && !m_findHintTimer.IsElapsed() ) + { + // too soon + return Continue(); + } + + m_findHintTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + // figure out where to teleport into the map + bool bShouldTeleportToHint = me->HasAttribute( CTFBot::TELEPORT_TO_HINT ); + bool bShouldCheckForBlockingObject = !m_bTeleportedToHint && bShouldTeleportToHint; + CHandle< CTFBotHintEngineerNest > newNest = NULL; + if ( !CTFBotMvMEngineerHintFinder::FindHint( bShouldCheckForBlockingObject, !bShouldTeleportToHint, &newNest ) ) + { + // try again next time + return Continue(); + } + + // unown the old nest + if ( m_nestHint ) + { + m_nestHint->SetOwnerEntity( NULL ); + } + + m_nestHint = newNest; + m_nestHint->SetOwnerEntity( me ); + m_sentryHint = m_nestHint->GetSentryHint(); + TakeOverStaleNest( m_sentryHint, me ); + + if ( me->GetTeleportWhere().Count() > 0 ) + { + m_teleporterHint = m_nestHint->GetTeleporterHint(); + TakeOverStaleNest( m_teleporterHint, me ); + } + } + + if ( !m_bTeleportedToHint && me->HasAttribute( CTFBot::TELEPORT_TO_HINT ) ) + { + m_nTeleportedCount++; + bool bFirstTeleportSpawn = m_nTeleportedCount == 1; + m_bTeleportedToHint = true; + return SuspendFor( new CTFBotMvMEngineerTeleportSpawn( m_nestHint, bFirstTeleportSpawn ), "In spawn area - teleport to the teleporter hint" ); + } + + const float rebuildInterval = 3.0f; + CObjectSentrygun *mySentry = NULL; + if ( m_sentryHint ) + { + if ( m_sentryHint->GetOwnerEntity() && m_sentryHint->GetOwnerEntity()->IsBaseObject() ) + { + mySentry = assert_cast< CObjectSentrygun* >( m_sentryHint->GetOwnerEntity() ); + } + + if ( mySentry ) + { + // force an interval between sentry being destroyed and me trying to rebuild it + m_sentryRebuildTimer.Start( rebuildInterval ); + } + else + { + // check if there's a stale object on the hint + if ( m_sentryHint->GetOwnerEntity() && m_sentryHint->GetOwnerEntity()->IsBaseObject() ) + { + mySentry = assert_cast< CObjectSentrygun* >( m_sentryHint->GetOwnerEntity() ); + me->AddObject( mySentry ); + mySentry->SetOwnerEntity( me ); + } + else + { + if ( m_sentryRebuildTimer.IsElapsed() ) + { + return SuspendFor( new CTFBotMvMEngineerBuildSentryGun( m_sentryHint ), "No sentry - building a new one" ); + } + else + { + // run away! + return SuspendFor( new CTFBotRetreatToCover( 1.0f ), "Lost my sentry - retreat!" ); + } + } + } + } + + if ( mySentry && mySentry->GetHealth() < mySentry->GetMaxHealth() && !mySentry->IsBuilding() ) + { + // track when sentry was last hurt + m_sentryInjuredTimer.Start( 3.0f ); + } + + + CObjectTeleporter *myTeleporter = NULL; + if ( m_teleporterHint && m_sentryInjuredTimer.IsElapsed() ) + { + if ( m_teleporterHint->GetOwnerEntity() && m_teleporterHint->GetOwnerEntity()->IsBaseObject() ) + { + // force an interval between teleporter being destroyed and me trying to rebuild it + myTeleporter = assert_cast< CObjectTeleporter* >( m_teleporterHint->GetOwnerEntity() ); + m_teleporterRebuildTimer.Start( rebuildInterval ); + } + else if ( m_teleporterRebuildTimer.IsElapsed() ) + { + return SuspendFor( new CTFBotMvMEngineerBuildTeleportExit( m_teleporterHint ), "Sentry is safe - building a teleport exit" ); + } + } + + // fix teleporter if sentry is not hurt + if ( myTeleporter && m_sentryInjuredTimer.IsElapsed() && myTeleporter->GetHealth() < myTeleporter->GetMaxHealth() && !myTeleporter->IsBuilding() ) + { + float rangeToTeleporter = me->GetDistanceBetween( myTeleporter ); + + const float nearTeleporterRange = 75.0f; + + if ( rangeToTeleporter < 1.2f * nearTeleporterRange ) + { + // crouch as I get close + me->PressCrouchButton(); + } + + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + Vector toTeleporter = myTeleporter->GetAbsOrigin() - me->GetAbsOrigin(); + Vector hittingTeleporterSpot = myTeleporter->GetAbsOrigin() - 50.0f * toTeleporter.Normalized(); + + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_path.Compute( me, hittingTeleporterSpot, cost ); + } + + m_path.Update( me ); + + if ( rangeToTeleporter < nearTeleporterRange ) + { + // we are in position - hit sentry with wrench + me->GetBodyInterface()->AimHeadTowards( myTeleporter->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Work on my Teleporter" ); + me->PressFireButton(); + } + } + else if ( mySentry ) + { + float rangeToSentry = me->GetDistanceBetween( mySentry ); + + const float nearSentryRange = 75.0f; + + if ( rangeToSentry < 1.2f * nearSentryRange ) + { + // crouch as I get close + me->PressCrouchButton(); + } + + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + Vector mySentryForward; + AngleVectors( mySentry->GetTurretAngles(), &mySentryForward ); + + Vector behindSentrySpot = mySentry->GetAbsOrigin() - 50.0f * mySentryForward; + + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_path.Compute( me, behindSentrySpot, cost ); + } + + m_path.Update( me ); + + if ( rangeToSentry < nearSentryRange ) + { + // we are in position - hit sentry with wrench + me->GetBodyInterface()->AimHeadTowards( mySentry->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Work on my Sentry" ); + me->PressFireButton(); + } + } + + TryToDetonateStaleNest(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotMvMEngineerIdle::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotMvMEngineerIdle::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotMvMEngineerIdle::ShouldHurry( const INextBot *me ) const +{ + return ANSWER_YES; +} + + +CTFBotHintEngineerNest* SelectOutOfRangeNest( const CUtlVector< CTFBotHintEngineerNest* >& nestVector ) +{ + if ( nestVector.Count() ) + { + for ( int i=0; i<nestVector.Count(); ++i ) + { + if ( nestVector[i]->IsStaleNest() ) + { + return nestVector[i]; + } + } + + int which = RandomInt( 0, nestVector.Count() - 1 ); + return nestVector[which]; + } + + return NULL; +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBotMvMEngineerHintFinder::FindHint( bool bShouldCheckForBlockingObjects, bool bAllowOutOfRangeNest, CHandle< CTFBotHintEngineerNest >* pFoundNest /*= NULL*/ ) +{ + // collect all existing and active teleporter hints + CUtlVector< CTFBotHintEngineerNest* > activeEngineerNest; + for ( int i=0; i<ITFBotHintEntityAutoList::AutoList().Count(); ++i ) + { + CBaseTFBotHintEntity *pHint = static_cast<CBaseTFBotHintEntity*>( ITFBotHintEntityAutoList::AutoList()[i] ); + if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_ENGINEER_NEST ) && pHint->IsEnabled() && pHint->GetOwnerEntity() == NULL ) + { + activeEngineerNest.AddToTail( static_cast< CTFBotHintEngineerNest* >( pHint ) ); + } + } + + if ( activeEngineerNest.Count() == 0 ) + { + if ( pFoundNest ) + { + *pFoundNest = NULL; + } + + return false; + } + + BombInfo_t bombInfo; + GetBombInfo( &bombInfo ); + + CUtlVector< CTFBotHintEngineerNest* > forwardOutOfRangeHintVector; + CUtlVector< CTFBotHintEngineerNest* > backwardOutOfRangeHintVector; + + CUtlVector< CTFBotHintEngineerNest* > freeAtFrontHintVector; + CUtlVector< CTFBotHintEngineerNest* > staleAtFrontHintVector; + for( int i=0; i<activeEngineerNest.Count(); ++i ) + { + CTFBotHintEngineerNest* pCurrentNest = activeEngineerNest[i]; + const Vector& vNestPosition = pCurrentNest->GetAbsOrigin(); + CTFNavArea *hintArea = (CTFNavArea *)TheNavMesh->GetNearestNavArea( vNestPosition, false, 1000.0f ); + if ( !hintArea ) + { + Warning( "Sentry hint has NULL nav area!\n" ); + continue; + } + + + float hintDistanceToTarget = hintArea->GetTravelDistanceToBombTarget(); + if ( hintDistanceToTarget > bombInfo.m_flMinBattleFront && hintDistanceToTarget < bombInfo.m_flMaxBattleFront ) + { + if ( bShouldCheckForBlockingObjects ) + { + // check for blocking players and objects + CBaseEntity *pList[256]; + int count = UTIL_EntitiesInBox( pList, ARRAYSIZE( pList ), vNestPosition + VEC_HULL_MIN, vNestPosition + VEC_HULL_MAX, FL_CLIENT|FL_OBJECT ); + if ( count > 0 ) + { + continue; + } + } + + // this hint is in range of the front + if ( pCurrentNest->IsStaleNest() ) + { + // some dead engineer was here and left his object(s) behind. I should take over + staleAtFrontHintVector.AddToTail( pCurrentNest ); + } + else + { + if ( VectorLength( bombInfo.m_vPosition - vNestPosition ) < tf_bot_engineer_mvm_hint_min_distance_from_bomb.GetFloat() ) + { + // the hint is too close to the bomb, don't go there + continue; + } + // this hint is also unowned + freeAtFrontHintVector.AddToTail( pCurrentNest ); + } + } + else if ( hintDistanceToTarget > bombInfo.m_flMaxBattleFront ) + { + forwardOutOfRangeHintVector.AddToTail( pCurrentNest ); + } + else + { + backwardOutOfRangeHintVector.AddToTail( pCurrentNest ); + } + } + + CTFBotHintEngineerNest *hint = NULL; + if ( freeAtFrontHintVector.Count() == 0 && staleAtFrontHintVector.Count() == 0 ) + { + if ( bAllowOutOfRangeNest ) + { + // try to advance forward before falling backward + hint = SelectOutOfRangeNest( forwardOutOfRangeHintVector ); + if ( !hint ) + { + hint = SelectOutOfRangeNest( backwardOutOfRangeHintVector ); + } + } + + // no hints are in range, or they are all in use + if ( pFoundNest ) + { + *pFoundNest = hint; + } + } + else + { + // try to pick stale nest in range first + if ( staleAtFrontHintVector.Count() ) + { + int whichHint = RandomInt( 0, staleAtFrontHintVector.Count()-1 ); + hint = staleAtFrontHintVector[ whichHint ]; + } + // if I didn't find any stale nest, try to find a free one + else if ( freeAtFrontHintVector.Count() ) + { + int whichHint = RandomInt( 0, freeAtFrontHintVector.Count()-1 ); + hint = freeAtFrontHintVector[ whichHint ]; + } + + if ( pFoundNest ) + { + *pFoundNest = hint; + } + } + + return hint != NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( tf_bot_mvm_show_engineer_hint_region, "Show the nav areas MvM engineer bots will consider when selecting sentry and teleporter hints", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + + trace_t result; + Vector forward; + pPlayer->EyeVectors( &forward ); + + UTIL_TraceLine( pPlayer->EyePosition(), + pPlayer->EyePosition() + forward * 10000.0f, MASK_SOLID, + pPlayer, COLLISION_GROUP_NONE, &result ); + + float flDrawTime = 5.0f; + + if ( result.DidHit() ) + { + CTFNavArea *area = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( result.endpos ); + + if ( area ) + { + float battlefront = area->GetTravelDistanceToBombTarget(); + + float maxBattlefront = battlefront + tf_bot_engineer_mvm_sentry_hint_bomb_backward_range.GetFloat(); + float minBattlefront = battlefront - tf_bot_engineer_mvm_sentry_hint_bomb_forward_range.GetFloat(); + + CUtlVector< CTFNavArea * > battlefrontAreaVector; + TheTFNavMesh()->CollectAreaWithinBombTravelRange( &battlefrontAreaVector, minBattlefront, maxBattlefront ); + + CUtlVector< CTFNavArea * > hintAreaVector; + for ( int i=0; i<ITFBotHintEntityAutoList::AutoList().Count(); ++i ) + { + CBaseTFBotHintEntity *pHint = static_cast< CBaseTFBotHintEntity* >( ITFBotHintEntityAutoList::AutoList()[i] ); + hintAreaVector.AddToTail( (CTFNavArea*)TheNavMesh->GetNearestNavArea( pHint ) ); + } + + for( int i=0; i<battlefrontAreaVector.Count(); ++i ) + { + CTFNavArea *fillArea = battlefrontAreaVector[i]; + + if ( fillArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE || TF_NAV_SPAWN_ROOM_RED ) ) + { + continue; + } + + fillArea->DrawFilled( 255, 100, 0, 0, flDrawTime ); + + for ( int j=0; j<hintAreaVector.Count(); ++j ) + { + if ( fillArea == hintAreaVector[j] ) + { + CBaseTFBotHintEntity *pHint = static_cast< CBaseTFBotHintEntity* >( ITFBotHintEntityAutoList::AutoList()[j] ); + Color color; + if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_SENTRYGUN ) ) + { + color = Color( 0, 255, 0 ); + } + else if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_TELEPORTER_EXIT ) ) + { + color = Color( 0, 0, 255 ); + } + else + { + bool bTooCloseToBomb = VectorLength( result.endpos - pHint->GetAbsOrigin() ) < tf_bot_engineer_mvm_hint_min_distance_from_bomb.GetFloat(); + color = bTooCloseToBomb ? Color( 255, 0, 0 ) : Color( 255, 255, 0 ); + } + NDebugOverlay::Sphere( pHint->GetAbsOrigin(), 50, color.r(), color.g(), color.b(), true, flDrawTime ); + } + } + } + + NDebugOverlay::Sphere( result.endpos, tf_bot_engineer_mvm_hint_min_distance_from_bomb.GetFloat(), 255, 255, 0, false, flDrawTime ); + } + } +} diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h new file mode 100644 index 0000000..13ea69f --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h @@ -0,0 +1,55 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// Michael Booth, September 2012 + +#ifndef TF_BOT_MVM_ENGINEER_IDLE_H +#define TF_BOT_MVM_ENGINEER_IDLE_H + +#include "Path/NextBotPathFollow.h" + +class CBaseTFBotHintEntity; +class CTFBotHintSentrygun; +class CTFBotHintTeleporterExit; +class CTFBotHintEngineerNest; + +class CTFBotMvMEngineerIdle : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + + virtual const char *GetName( void ) const { return "MvMEngineerIdle"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; + CountdownTimer m_sentryInjuredTimer; + CountdownTimer m_sentryRebuildTimer; + CountdownTimer m_teleporterRebuildTimer; + CountdownTimer m_findHintTimer; + CountdownTimer m_reevaluateNestTimer; + + int m_nTeleportedCount; + bool m_bTeleportedToHint; + CHandle< CTFBotHintTeleporterExit > m_teleporterHint; + CHandle< CTFBotHintSentrygun > m_sentryHint; + CHandle< CTFBotHintEngineerNest > m_nestHint; + + void TakeOverStaleNest( CBaseTFBotHintEntity* pHint, CTFBot *me ); + bool ShouldAdvanceNestSpot( CTFBot *me ); + + void TryToDetonateStaleNest(); + bool m_bTriedToDetonateStaleNest; +}; + +class CTFBotMvMEngineerHintFinder +{ +public: + static bool FindHint( bool bShouldCheckForBlockingObjects, bool bAllowOutOfRangeNest, CHandle< CTFBotHintEngineerNest >* pFoundNest = NULL ); +}; + + +#endif // TF_BOT_MVM_ENGINEER_IDLE_H diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.cpp b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.cpp new file mode 100644 index 0000000..5b439e6 --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.cpp @@ -0,0 +1,95 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_obj.h" +#include "tf_gamerules.h" +#include "bot/tf_bot.h" +#include "tf_obj_sentrygun.h" +#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.h" +#include "bot/map_entities/tf_bot_hint_entity.h" +#include "string_t.h" +#include "tf_fx.h" +#include "player_vs_environment/tf_population_manager.h" + +//--------------------------------------------------------------------------------------------- +CTFBotMvMEngineerTeleportSpawn::CTFBotMvMEngineerTeleportSpawn( CBaseTFBotHintEntity* pHint, bool bFirstTeleportSpawn ) +{ + m_hintEntity = pHint; + m_bFirstTeleportSpawn = bFirstTeleportSpawn; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMvMEngineerTeleportSpawn::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + if ( !me->HasAttribute( CTFBot::TELEPORT_TO_HINT ) ) + { + return Done( "Cannot teleport to hint with out Attributes TeleportToHint" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMvMEngineerTeleportSpawn::Update( CTFBot *me, float interval ) +{ + if ( !m_teleportDelay.HasStarted() ) + { + m_teleportDelay.Start( 0.1f ); + if ( m_hintEntity ) + TFGameRules()->PushAllPlayersAway( m_hintEntity->GetAbsOrigin(), 400, 500, TF_TEAM_RED ); + } + else if ( m_teleportDelay.IsElapsed() ) + { + if ( !m_hintEntity ) + return Done( "Cannot teleport to hint as m_hintEntity is NULL" ); + + // teleport the engineer to the sentry spawn point + QAngle angles = m_hintEntity->GetAbsAngles(); + Vector origin = m_hintEntity->GetAbsOrigin(); + origin.z += 10.f; // move up off the around a little bit to prevent the engineer from getting stuck in the ground + + me->Teleport( &origin, &angles, NULL ); + + CPVSFilter filter( origin ); + TE_TFParticleEffect( filter, 0.0, "teleported_blue", origin, vec3_angle ); + TE_TFParticleEffect( filter, 0.0, "player_sparkles_blue", origin, vec3_angle ); + + if ( m_bFirstTeleportSpawn ) + { + // notify players that engineer's teleported into the map + TE_TFParticleEffect( filter, 0.0, "teleported_mvm_bot", origin, vec3_angle ); + me->EmitSound( "Engineer.MVM_BattleCry07" ); + m_hintEntity->EmitSound( "MVM.Robot_Engineer_Spawn" ); + + if ( g_pPopulationManager ) + { + CWave *pWave = g_pPopulationManager->GetCurrentWave(); + if ( pWave ) + { + if ( pWave->NumEngineersTeleportSpawned() == 0 ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_First_Engineer_Teleport_Spawned" ); + } + else + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Another_Engineer_Teleport_Spawned" ); + } + + pWave->IncrementEngineerTeleportSpawned(); + } + } + } + + return Done( "Teleported" ); + } + + return Continue(); +} + diff --git a/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.h b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.h new file mode 100644 index 0000000..cd2f32b --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_teleport_spawn.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= + +#ifndef TF_BOT_MVM_ENGINEER_TELEPORT_SPAWN_H +#define TF_BOT_MVM_ENGINEER_TELEPORT_SPAWN_H + +class CBaseTFBotHintEntity; + +class CTFBotMvMEngineerTeleportSpawn : public Action< CTFBot > +{ +public: + CTFBotMvMEngineerTeleportSpawn( CBaseTFBotHintEntity* pHint, bool bFirstTeleportSpawn ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "MvMEngineerTeleportSpawn"; }; + +private: + CountdownTimer m_teleportDelay; + CHandle< CBaseTFBotHintEntity > m_hintEntity; + bool m_bFirstTeleportSpawn; +}; + +#endif // TF_BOT_MVM_ENGINEER_TELEPORT_SPAWN_H diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.cpp new file mode 100644 index 0000000..e6d586e --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.cpp @@ -0,0 +1,118 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_build.cpp +// Engineer building his buildings +// Michael Booth, February 2009 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_obj.h" +#include "tf_obj_sentrygun.h" +#include "tf_obj_dispenser.h" +#include "tf_gamerules.h" +#include "tf_weapon_builder.h" +#include "bot/tf_bot.h" +#include "bot/behavior/engineer/tf_bot_engineer_build.h" +#include "bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.h" +#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h" + + +#include "raid/tf_raid_logic.h" + +// this was useful when engineers build at their normal (slow) rate to make sure initial sentries get built in time +ConVar tf_raid_engineer_infinte_metal( "tf_raid_engineer_infinte_metal", "1", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); + + +//--------------------------------------------------------------------------------------------- +Action< CTFBot > *CTFBotEngineerBuild::InitialContainedAction( CTFBot *me ) +{ + if ( TFGameRules()->IsPVEModeActive() ) + { + return new CTFBotEngineerMoveToBuild; + } + + return new CTFBotEngineerBuildTeleportEntrance; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuild::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuild::Update( CTFBot *me, float interval ) +{ + if ( TFGameRules()->IsPVEModeActive() && tf_raid_engineer_infinte_metal.GetBool() ) + { + // infinite ammo + me->GiveAmmo( 1000, TF_AMMO_METAL, true ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuild::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEngineerBuild::OnTerritoryLost( CTFBot *me, int territoryID ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +// Hack to disable ammo/health gathering elsewhere +QueryResultType CTFBotEngineerBuild::ShouldHurry( const INextBot *meBot ) const +{ + CTFBot *me = (CTFBot *)meBot->GetEntity(); + + CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN ); + CObjectDispenser *myDispenser = (CObjectDispenser *)me->GetObjectOfType( OBJ_DISPENSER ); + + if ( mySentry && myDispenser && !mySentry->IsBuilding() && !myDispenser->IsBuilding() && me->GetActiveTFWeapon() && me->GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_WRENCH ) + { + if ( me->IsAmmoLow() && myDispenser->GetAvailableMetal() <= 0 ) + { + // we're totally out of metal - collect some nearby + return ANSWER_NO; + } + + // by being in a "hurry" we wont collect health and ammo + return ANSWER_YES; + } + + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotEngineerBuild::ShouldAttack( const INextBot *meBot, const CKnownEntity *them ) const +{ + CTFBot *me = (CTFBot *)meBot->GetEntity(); + CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN ); + + CTFPlayer *themPlayer = ToTFPlayer( them->GetEntity() ); + + if ( themPlayer && themPlayer->IsPlayerClass( TF_CLASS_SPY ) ) + { + // Engineers hate Spies + return ANSWER_YES; + } + + if ( mySentry && me->IsRangeLessThan( mySentry, 100.0f ) ) + { + // focus on keeping our sentry alive + return ANSWER_NO; + } + + return ANSWER_UNDEFINED; +} diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.h new file mode 100644 index 0000000..2ff6265 --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build.h @@ -0,0 +1,31 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_build.h +// Engineer building his buildings +// Michael Booth, February 2009 + +#ifndef TF_BOT_ENGINEER_BUILD_H +#define TF_BOT_ENGINEER_BUILD_H + +class CTFBotHintTeleporterExit; + + +class CTFBotEngineerBuild : public Action< CTFBot > +{ +public: + virtual Action< CTFBot > *InitialContainedAction( CTFBot *me ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID ); + + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "EngineerBuild"; }; +}; + + +#endif // TF_BOT_ENGINEER_BUILD_H diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.cpp new file mode 100644 index 0000000..eabd9ba --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.cpp @@ -0,0 +1,212 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_build_dispenser.cpp +// Engineer building his Dispenser near his Sentry +// Michael Booth, May 2010 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_obj.h" +#include "tf_obj_sentrygun.h" +#include "tf_obj_dispenser.h" +#include "tf_gamerules.h" +#include "tf_weapon_builder.h" +#include "bot/tf_bot.h" +#include "bot/behavior/engineer/tf_bot_engineer_build_dispenser.h" +#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h" +#include "bot/behavior/tf_bot_get_ammo.h" + + +extern ConVar tf_bot_path_lookahead_range; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuildDispenser::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_placementTriesLeft = 3; + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +class PressFireButtonIfValidBuildPositionReply : public INextBotReply +{ +public: + PressFireButtonIfValidBuildPositionReply( void ) + { + m_builder = NULL; + } + + void SetBuilder( CTFWeaponBuilder *builder ) + { + m_builder = builder; + } + + // invoked when process completed successfully + virtual void OnSuccess( INextBot *bot ) + { + if ( m_builder != NULL && m_builder->IsValidPlacement() ) + { + INextBotPlayerInput *playerInput = dynamic_cast< INextBotPlayerInput * >( bot->GetEntity() ); + if ( playerInput ) + { + playerInput->PressFireButton(); + } + } + } + + CTFWeaponBuilder *m_builder; +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuildDispenser::Update( CTFBot *me, float interval ) +{ + if ( me->GetTimeSinceLastInjury() < 1.0f ) + { + return Done( "Ouch! I'm under attack" ); + } + + CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN ); + if ( !mySentry ) + { + return Done( "No Sentry" ); + } + + if ( mySentry->GetTimeSinceLastInjury() < 1.0f || mySentry->GetHealth() < mySentry->GetMaxHealth() ) + { + return Done( "Need to repair my Sentry" ); + } + + CObjectDispenser *myDispenser = (CObjectDispenser *)me->GetObjectOfType( OBJ_DISPENSER ); + if ( myDispenser ) + { + return Done( "Dispenser built" ); + } + + if ( m_placementTriesLeft <= 0 ) + { + return Done( "Can't find a place to build a Dispenser" ); + } + + if ( me->CanBuild( OBJ_DISPENSER ) == CB_NEED_RESOURCES ) + { + if ( m_getAmmoTimer.IsElapsed() && CTFBotGetAmmo::IsPossible( me ) ) + { + // need more metal - get some + m_getAmmoTimer.Start( 1.0f ); + return SuspendFor( new CTFBotGetAmmo, "Need more metal to build" ); + } +/* + else + { + // work on my sentry while I wait for ammo to show up + me->GetBodyInterface()->AimHeadTowards( mySentry->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Work on sentry while I wait for ammo to show up" ); + me->PressFireButton(); + return Continue(); + } +*/ + } + + +/* + // if my sentry is under attack, forgo building a dispenser - focus on keeping the sentry alive + if ( mySentry->GetTimeSinceLastInjury() < 1.0f ) + { + CBaseCombatWeapon *wrench = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE ); + if ( wrench ) + { + me->Weapon_Switch( wrench ); + } + + me->GetBodyInterface()->AimHeadTowards( mySentry->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Focusing on keeping my besieged sentry alive" ); + me->PressFireButton(); + + return Continue(); + } +*/ + + + // move behind the Sentry (our chosen build location) + Vector buildSpot = mySentry->GetAbsOrigin() - 75.0f * mySentry->BodyDirection2D(); + + // the ground might be steeply sloped (ie: stairs), so find the actual ground + buildSpot.z += HumanHeight; + TheNavMesh->GetSimpleGroundHeight( buildSpot, &buildSpot.z ); + + if ( me->IsDistanceBetweenLessThan( buildSpot, 100.0f ) ) + { + // crouch as we get close so we slow down and hit our mark + me->PressCrouchButton(); + } + + // if too far away from our build location, move closer + if ( me->IsDistanceBetweenGreaterThan( buildSpot, 25.0f ) ) + { + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, buildSpot, cost ); + } + + m_path.Update( me ); + + return Continue(); + } + + // we're at our build spot behind our sentry now - build a Dispenser + CTFWeaponBuilder *builder = dynamic_cast< CTFWeaponBuilder * >( me->GetActiveTFWeapon() ); + if ( !builder || builder->GetType() != OBJ_DISPENSER || builder->m_hObjectBeingBuilt == NULL ) + { + // at home position, build the object + me->StartBuildingObjectOfType( OBJ_DISPENSER ); + } + else if ( m_searchTimer.IsElapsed() ) + { + // rotate to find valid spot + Vector toSentry = mySentry->GetAbsOrigin() - me->GetAbsOrigin(); + toSentry.NormalizeInPlace(); + + Vector forward = -toSentry; + + float angle = RandomFloat( -M_PI/2.0f, M_PI/2.0f ); + float s, c; + FastSinCos( angle, &s, &c ); + + forward.x = toSentry.x * c - toSentry.y * s; + forward.y = toSentry.x * s + toSentry.y * c; + forward.z = 0.0f; + + static PressFireButtonIfValidBuildPositionReply buildReply; + + buildReply.SetBuilder( builder ); + me->GetBodyInterface()->AimHeadTowards( me->EyePosition() - 100.0f * forward, IBody::CRITICAL, 1.0f, &buildReply, "Trying to place my dispenser" ); + + m_searchTimer.Start( 1.0f ); + + --m_placementTriesLeft; + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotEngineerBuildDispenser::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + me->GetBodyInterface()->ClearPendingAimReply(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuildDispenser::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_path.Invalidate(); + m_repathTimer.Invalidate(); + me->GetBodyInterface()->ClearPendingAimReply(); + + return Continue(); +} + diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.h new file mode 100644 index 0000000..12e5dbf --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_dispenser.h @@ -0,0 +1,31 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_build_dispenser.h +// Engineer building his Dispenser near his Sentry +// Michael Booth, May 2010 + +#ifndef TF_BOT_ENGINEER_BUILD_DISPENSER_H +#define TF_BOT_ENGINEER_BUILD_DISPENSER_H + + +class CTFBotEngineerBuildDispenser : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual const char *GetName( void ) const { return "EngineerBuildDispenser"; }; + +private: + CountdownTimer m_searchTimer; + CountdownTimer m_getAmmoTimer; + CountdownTimer m_repathTimer; + + int m_placementTriesLeft; + PathFollower m_path; +}; + + +#endif // TF_BOT_ENGINEER_BUILD_DISPENSER_H diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.cpp new file mode 100644 index 0000000..b1beb35 --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.cpp @@ -0,0 +1,194 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_build_sentrygun.cpp +// Engineer building his Sentry gun +// Michael Booth, May 2010 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_obj.h" +#include "tf_obj_sentrygun.h" +#include "tf_obj_dispenser.h" +#include "tf_gamerules.h" +#include "tf_weapon_builder.h" +#include "bot/tf_bot.h" +#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h" +#include "bot/behavior/engineer/tf_bot_engineer_build_sentrygun.h" +#include "bot/behavior/tf_bot_get_ammo.h" +#include "bot/map_entities/tf_bot_hint_sentrygun.h" + +extern ConVar tf_bot_path_lookahead_range; + + +//--------------------------------------------------------------------------------------------- +CTFBotEngineerBuildSentryGun::CTFBotEngineerBuildSentryGun( void ) +{ + m_sentryBuildHint = NULL; +} + + +//--------------------------------------------------------------------------------------------- +CTFBotEngineerBuildSentryGun::CTFBotEngineerBuildSentryGun( CTFBotHintSentrygun *sentryBuildHint ) +{ + m_sentryBuildHint = sentryBuildHint; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuildSentryGun::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_sentryTriesLeft = 5; + m_giveUpTimer.Invalidate(); + + m_searchTimer.Invalidate(); + m_wanderWay = 1; + m_needToAimSentry = true; + + m_sentryBuildLocation = ( m_sentryBuildHint == NULL ) ? me->GetAbsOrigin() : m_sentryBuildHint->GetAbsOrigin(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuildSentryGun::Update( CTFBot *me, float interval ) +{ + if ( me->GetTimeSinceLastInjury() < 1.0f ) + { + return Done( "Ouch! I'm under attack" ); + } + + CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN ); + if ( mySentry ) + { + return Done( "Sentry built" ); + } + + // collect metal as we move to our build location + if ( me->CanBuild( OBJ_SENTRYGUN ) == CB_NEED_RESOURCES ) + { + if ( m_getAmmoTimer.IsElapsed() && CTFBotGetAmmo::IsPossible( me ) ) + { + // need more metal - get some + m_getAmmoTimer.Start( 1.0f ); + return SuspendFor( new CTFBotGetAmmo, "Need more metal to build my Sentry" ); + } + } + + // various interruptions could mean we're away from our build location - move to it + if ( me->IsRangeGreaterThan( m_sentryBuildLocation, 25.0f ) ) + { + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, m_sentryBuildLocation, cost ); + } + + m_path.Update( me ); + + if ( !m_path.IsValid() ) + { + return Done( "Path failed" ); + } + + return Continue(); + } + + // we are at our build location + if ( m_sentryTriesLeft <= 0 ) + { + // couldn't build here + return Done( "Couldn't find a place to build" ); + } + + // attempt to build a Sentry here + if ( m_sentryBuildHint != NULL ) + { + // directly create a sentry gun at the precise position and orientation desired + CObjectSentrygun *mySentry = (CObjectSentrygun *)CreateEntityByName( "obj_sentrygun" ); + if ( mySentry ) + { + m_sentryBuildHint->IncrementUseCount(); + + mySentry->SetAbsOrigin( m_sentryBuildHint->GetAbsOrigin() ); + mySentry->SetAbsAngles( QAngle( 0, m_sentryBuildHint->GetAbsAngles().y, 0 ) ); + mySentry->Spawn(); + + mySentry->StartPlacement( me ); + mySentry->StartBuilding( me ); + } + } + else + { + // no precise build location - go through the normal build process + + CTFWeaponBuilder *builder = dynamic_cast< CTFWeaponBuilder * >( me->GetActiveTFWeapon() ); + if ( !builder || builder->GetType() != OBJ_SENTRYGUN || builder->m_hObjectBeingBuilt == NULL ) + { + // at home position, build a sentry (switch to the sentry builder "gun") + me->StartBuildingObjectOfType( OBJ_SENTRYGUN ); + return Continue(); + } + + // orient sentry towards where enemies enter this region + if ( m_needToAimSentry ) + { + CTFNavArea *myArea = (CTFNavArea *)me->GetLastKnownArea(); + if ( myArea ) + { + CUtlVector< CTFNavArea * > invasionVector; + myArea->GetEnemyInvasionAreaVector( me->GetTeamNumber() ); + + if ( invasionVector.Count() > 0 ) + { + // orient sentry towards where enemies enter this region + int which = RandomInt( 0, invasionVector.Count()-1 ); + me->GetBodyInterface()->AimHeadTowards( invasionVector[ which ]->GetCenter(), IBody::CRITICAL, 1.0f, NULL, "Sentry build orientation" ); + m_needToAimSentry = false; + } + } + } + + if ( me->GetBodyInterface()->IsHeadSteady() ) + { + if ( builder->IsValidPlacement() ) + { + // build the sentry + me->PressFireButton(); + } + else + { + // move around a bit to find valid spot + if ( m_searchTimer.IsElapsed() ) + { + m_wanderWay = RandomInt( 0, 3 ); + m_needToAimSentry = true; + m_searchTimer.Start( RandomFloat( 0.1f, 0.25f ) ); + --m_sentryTriesLeft; + } + + switch( m_wanderWay ) + { + case 0: me->PressForwardButton(); break; + case 1: me->PressBackwardButton(); break; + case 2: me->PressRightButton(); break; + case 3: me->PressLeftButton(); break; + } + } + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuildSentryGun::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_path.Invalidate(); + m_repathTimer.Invalidate(); + + return Continue(); +} diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.h new file mode 100644 index 0000000..774b7ca --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_sentrygun.h @@ -0,0 +1,44 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_build_sentrygun.h +// Engineer building his Sentry gun +// Michael Booth, May 2010 + +#ifndef TF_BOT_ENGINEER_BUILD_SENTRYGUN_H +#define TF_BOT_ENGINEER_BUILD_SENTRYGUN_H + +class CTFBotHintSentrygun; + + +class CTFBotEngineerBuildSentryGun : public Action< CTFBot > +{ +public: + CTFBotEngineerBuildSentryGun( void ); + CTFBotEngineerBuildSentryGun( CTFBotHintSentrygun *sentryBuildHint ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual const char *GetName( void ) const { return "EngineerBuildSentryGun"; }; + +private: + CountdownTimer m_searchTimer; + CountdownTimer m_giveUpTimer; + CountdownTimer m_getAmmoTimer; + CountdownTimer m_repathTimer; + CountdownTimer m_buildTeleporterExitTimer; + + int m_sentryTriesLeft; + PathFollower m_path; + + CTFBotHintSentrygun *m_sentryBuildHint; + Vector m_sentryBuildLocation; + + int m_wanderWay; + bool m_needToAimSentry; + Vector m_sentryBuildAimTarget; +}; + + +#endif // TF_BOT_ENGINEER_BUILD_SENTRYGUN_H diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.cpp new file mode 100644 index 0000000..c6924aa --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.cpp @@ -0,0 +1,112 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_build_teleport_entrance.cpp +// Engineer building a teleport entrance right outside of the spawn room +// Michael Booth, May 2009 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_obj.h" +#include "tf_weapon_builder.h" +#include "bot/tf_bot.h" +#include "bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.h" +#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h" +#include "bot/behavior/tf_bot_get_ammo.h" + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_max_teleport_entrance_travel( "tf_bot_max_teleport_entrance_travel", "1500", FCVAR_CHEAT, "Don't plant teleport entrances farther than this travel distance from our spawn room" ); +ConVar tf_bot_teleport_build_surface_normal_limit( "tf_bot_teleport_build_surface_normal_limit", "0.99", FCVAR_CHEAT, "If the ground normal Z component is less that this value, Engineer bots won't place their entrance teleporter" ); + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuildTeleportEntrance::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuildTeleportEntrance::Update( CTFBot *me, float interval ) +{ + CTeamControlPoint *point = me->GetMyControlPoint(); + if ( !point ) + { + // wait until a control point becomes available + return Continue(); + } + + CTFNavArea *myArea = me->GetLastKnownArea(); + + if ( !myArea ) + { + return Done( "No nav mesh!" ); + } + + if ( myArea->GetIncursionDistance( me->GetTeamNumber() ) > tf_bot_max_teleport_entrance_travel.GetFloat() ) + { + return ChangeTo( new CTFBotEngineerMoveToBuild, "Too far from our spawn room to build teleporter entrance" ); + } + + // make sure we go back to our resupply cabinet after planting the teleporter entrance before we move on + if ( !me->IsAmmoFull() && CTFBotGetAmmo::IsPossible( me ) ) + { + return SuspendFor( new CTFBotGetAmmo, "Refilling ammo" ); + } + + CBaseObject *myTeleportEntrance = me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_ENTRANCE ); + if ( myTeleportEntrance ) + { + // successfully built + return ChangeTo( new CTFBotEngineerMoveToBuild, "Teleport entrance built" ); + } + + // head towards the control point and build as soon as we can + if ( !m_path.IsValid() ) + { + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, point->GetAbsOrigin(), cost ); + } + + m_path.Update( me ); + + // build + CTFWeaponBase *myGun = me->GetActiveTFWeapon(); + if ( myGun ) + { + CTFWeaponBuilder *builder = dynamic_cast< CTFWeaponBuilder * >( myGun ); + if ( builder ) + { + // don't build on slopes - causes player blockages + Vector forward; + me->EyeVectors( &forward ); + + const float placementRange = 30.0f; + forward *= placementRange; + + trace_t result; + UTIL_TraceLine( me->WorldSpaceCenter() + Vector( forward.x, forward.y, 0.0f ), me->WorldSpaceCenter() + Vector( forward.x, forward.y, -200.0f ), MASK_PLAYERSOLID, me, COLLISION_GROUP_NONE, &result ); + + if ( builder->IsValidPlacement() && result.DidHit() && result.plane.normal.z > tf_bot_teleport_build_surface_normal_limit.GetFloat() ) + { + // place it down + me->PressFireButton(); + } + } + else + { + // switch to teleporter builder + me->StartBuildingObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_ENTRANCE ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEngineerBuildTeleportEntrance::OnStuck( CTFBot *me ) +{ + m_path.Invalidate(); + + return TryContinue(); +} diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.h new file mode 100644 index 0000000..cf59418 --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_entrance.h @@ -0,0 +1,23 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_build_teleport_entrance.h +// Engineer building a teleport entrance right outside of the spawn room +// Michael Booth, May 2009 + +#ifndef TF_BOT_ENGINEER_BUILD_TELEPORT_ENTRANCE_H +#define TF_BOT_ENGINEER_BUILD_TELEPORT_ENTRANCE_H + +class CTFBotEngineerBuildTeleportEntrance : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + + virtual const char *GetName( void ) const { return "EngineerBuildTeleportEntrance"; }; + +private: + PathFollower m_path; +}; + +#endif // TF_BOT_ENGINEER_BUILD_TELEPORT_ENTRANCE_H diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.cpp new file mode 100644 index 0000000..4c621a5 --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.cpp @@ -0,0 +1,190 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_build_teleport_exit.cpp +// Engineer building a teleport exit +// Michael Booth, May 2010 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_obj.h" +#include "tf_obj_sentrygun.h" +#include "tf_weapon_builder.h" +#include "bot/tf_bot.h" +#include "bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h" +#include "bot/behavior/tf_bot_get_ammo.h" + +extern ConVar tf_bot_path_lookahead_range; + + +//--------------------------------------------------------------------------------------------- +CTFBotEngineerBuildTeleportExit::CTFBotEngineerBuildTeleportExit( void ) +{ + m_hasPreciseBuildLocation = false; +} + + +//--------------------------------------------------------------------------------------------- +CTFBotEngineerBuildTeleportExit::CTFBotEngineerBuildTeleportExit( const Vector &buildLocation, float buildAngle ) +{ + m_hasPreciseBuildLocation = true; + m_buildLocation = buildLocation; + m_buildAngle = buildAngle; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuildTeleportExit::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + if ( !m_hasPreciseBuildLocation ) + { + // if no specific build location given, just build right where we are + m_buildLocation = me->GetAbsOrigin(); + } + + m_giveUpTimer.Start( 3.1f ); + m_path.Invalidate(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuildTeleportExit::Update( CTFBot *me, float interval ) +{ + if ( me->GetTimeSinceLastInjury() < 1.0f ) + { + return Done( "Ouch! I'm under attack" ); + } + + CBaseObject *myTeleportEntrance = me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT ); + if ( myTeleportEntrance ) + { + // successfully built + return Done( "Teleport exit built" ); + } + + + // collect metal as we move to our build location + if ( me->CanBuild( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT ) == CB_NEED_RESOURCES ) + { + if ( m_getAmmoTimer.IsElapsed() && CTFBotGetAmmo::IsPossible( me ) ) + { + // need more metal - get some + m_getAmmoTimer.Start( 1.0f ); + return SuspendFor( new CTFBotGetAmmo, "Need more metal to build my Teleporter Exit" ); + } + } + + // move near our build position + const float buildRange = 50.0f; + if ( me->IsRangeGreaterThan( m_buildLocation, buildRange ) ) + { + // move into position + if ( !m_path.IsValid() || m_repathTimer.IsElapsed() ) + { + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, m_buildLocation, cost ); + + m_repathTimer.Start( RandomFloat( 2.0f, 3.0f ) ); + } + + m_path.Update( me ); + + // don't give up until we've reached our build location + m_giveUpTimer.Reset(); + + return Continue(); + } + + // in position to build + if ( m_giveUpTimer.IsElapsed() ) + { + return Done( "Taking too long - giving up" ); + } + + if ( m_hasPreciseBuildLocation ) + { + me->GetBodyInterface()->AimHeadTowards( m_buildLocation, IBody::CRITICAL, 1.0f, NULL, "Looking toward my precise build location" ); + + // directly create a teleporter exit at the precise position and orientation desired + CObjectTeleporter *myTeleporterExit = (CObjectTeleporter *)CreateEntityByName( "obj_teleporter" ); + if ( myTeleporterExit ) + { + myTeleporterExit->SetObjectMode( MODE_TELEPORTER_EXIT ); + myTeleporterExit->SetAbsOrigin( m_buildLocation ); + myTeleporterExit->SetAbsAngles( QAngle( 0, m_buildAngle, 0 ) ); + myTeleporterExit->Spawn(); + + myTeleporterExit->StartPlacement( me ); + myTeleporterExit->StartBuilding( me ); + myTeleporterExit->SetBuilder( me ); + + // teleporter exits are solid blockers - put engineer on top of exit or he'll be stuck + Vector myNewOrigin = me->GetAbsOrigin(); + myNewOrigin.z += me->GetLocomotionInterface()->GetStepHeight(); + + me->SetAbsOrigin( myNewOrigin ); + + return Done( "Teleport exit built at precise location" ); + } + + return Continue(); + } + + + // build exit roughly at this spot + CTFWeaponBase *myGun = me->GetActiveTFWeapon(); + if ( myGun ) + { + CTFWeaponBuilder *builder = dynamic_cast< CTFWeaponBuilder * >( myGun ); + if ( builder ) + { + if ( builder->IsValidPlacement() ) + { + // place it down + me->PressFireButton(); + } + else if ( m_searchTimer.IsElapsed() ) + { + // rotate to find valid spot + Vector forward; + float angle = RandomFloat( -M_PI, M_PI ); + FastSinCos( angle, &forward.y, &forward.x ); + + forward.z = 0.0f; + + me->GetBodyInterface()->AimHeadTowards( me->EyePosition() - 100.0f * forward, IBody::CRITICAL, 1.0f, NULL, "Trying to place my teleport exit" ); + + m_searchTimer.Start( 1.0f ); + } + } + else + { + // switch to teleporter builder + me->StartBuildingObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuildTeleportExit::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_giveUpTimer.Reset(); + m_path.Invalidate(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEngineerBuildTeleportExit::OnStuck( CTFBot *me ) +{ + m_path.Invalidate(); + + return TryContinue(); +} + + diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h new file mode 100644 index 0000000..d64f34a --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h @@ -0,0 +1,36 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_build_teleport_exit.h +// Engineer building a teleport exit +// Michael Booth, May 2010 + +#ifndef TF_BOT_ENGINEER_BUILD_TELEPORT_EXIT_H +#define TF_BOT_ENGINEER_BUILD_TELEPORT_EXIT_H + +class CTFBotEngineerBuildTeleportExit : public Action< CTFBot > +{ +public: + CTFBotEngineerBuildTeleportExit( void ); + CTFBotEngineerBuildTeleportExit( const Vector &buildLocation, float buildAngle ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + + virtual const char *GetName( void ) const { return "EngineerBuildTeleportExit"; }; + +private: + PathFollower m_path; + + bool m_hasPreciseBuildLocation; + Vector m_buildLocation; + float m_buildAngle; + + CountdownTimer m_giveUpTimer; + CountdownTimer m_repathTimer; + CountdownTimer m_getAmmoTimer; + CountdownTimer m_searchTimer; +}; + +#endif // TF_BOT_ENGINEER_BUILD_TELEPORT_EXIT_H diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.cpp new file mode 100644 index 0000000..a0f84c0 --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.cpp @@ -0,0 +1,497 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_building.cpp +// At building location, constructing buildings +// Michael Booth, May 2010 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_obj.h" +#include "tf_obj_sentrygun.h" +#include "tf_obj_dispenser.h" +#include "tf_gamerules.h" +#include "tf_weapon_builder.h" +#include "team_train_watcher.h" +#include "bot/tf_bot.h" +#include "bot/behavior/engineer/tf_bot_engineer_building.h" +#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h" +#include "bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h" +#include "bot/behavior/engineer/tf_bot_engineer_build_sentrygun.h" +#include "bot/behavior/engineer/tf_bot_engineer_build_dispenser.h" +#include "bot/behavior/tf_bot_attack.h" +#include "bot/behavior/tf_bot_get_ammo.h" +#include "bot/map_entities/tf_bot_hint_teleporter_exit.h" +#include "bot/map_entities/tf_bot_hint_sentrygun.h" +#include "NextBotUtil.h" + + +ConVar tf_bot_engineer_retaliate_range( "tf_bot_engineer_retaliate_range", "750", FCVAR_CHEAT, "If attacker who destroyed sentry is closer than this, attack. Otherwise, retreat" ); +ConVar tf_bot_engineer_exit_near_sentry_range( "tf_bot_engineer_exit_near_sentry_range", "2500", FCVAR_CHEAT, "Maximum travel distance between a bot's Sentry gun and its Teleporter Exit" ); +ConVar tf_bot_engineer_max_sentry_travel_distance_to_point( "tf_bot_engineer_max_sentry_travel_distance_to_point", "2500", FCVAR_CHEAT, "Maximum travel distance between a bot's Sentry gun and the currently contested point" ); + +extern ConVar tf_bot_path_lookahead_range; + +const int MaxPlacementAttempts = 5; + + +//--------------------------------------------------------------------------------------------- +CTFBotEngineerBuilding::CTFBotEngineerBuilding( void ) +{ + m_sentryBuildHint = NULL; +} + + +//--------------------------------------------------------------------------------------------- +CTFBotEngineerBuilding::CTFBotEngineerBuilding( CTFBotHintSentrygun *sentryBuildHint ) +{ + m_sentryBuildHint = sentryBuildHint; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuilding::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_sentryTriesLeft = MaxPlacementAttempts; + + m_territoryRangeTimer.Invalidate(); + + m_hasBuiltSentry = false; + m_isSentryOutOfPosition = false; + m_nearbyMetalStatus = NEARBY_METAL_UNKNOWN; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +// Everything is built, upgrade/maintain it +// TODO: Upgrade/maintain nearby friendly buildings, too. +void CTFBotEngineerBuilding::UpgradeAndMaintainBuildings( CTFBot *me ) +{ + CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN ); + CObjectDispenser *myDispenser = (CObjectDispenser *)me->GetObjectOfType( OBJ_DISPENSER ); + + if ( !mySentry ) + { + return; + } + + CBaseCombatWeapon *wrench = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE ); + if ( wrench ) + { + me->Weapon_Switch( wrench ); + } + + const float tooFarRange = 75.0f; + + if ( !myDispenser ) + { + // just work on our sentry + float rangeToSentry = me->GetDistanceBetween( mySentry ); + + if ( rangeToSentry < 1.2f * tooFarRange ) + { + // crouch both for cover behind our buildings, but also to slow us down so we hit our move goal more accurately + me->PressCrouchButton(); + } + + if ( rangeToSentry > tooFarRange ) + { + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, mySentry->GetAbsOrigin(), cost ); + } + + m_path.Update( me ); + } + else + { + // we are in position - work on our buildings + me->StopLookingAroundForEnemies(); + me->GetBodyInterface()->AimHeadTowards( mySentry->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Work on my Sentry" ); + me->PressFireButton(); + } + + return; + } + + // sit near both buildings + Vector betweenMyBuildings = ( mySentry->GetAbsOrigin() + myDispenser->GetAbsOrigin() ) / 2.0f; + + // try to equalize distance between both + float rangeToSentry = me->GetDistanceBetween( mySentry ); + float rangeToDispenser = me->GetDistanceBetween( myDispenser ); + + const float equalTolerance = 25.0f; + + if ( rangeToSentry < 1.2f * tooFarRange && rangeToDispenser < 1.2f * tooFarRange ) + { + // crouch both for cover behind our buildings, but also to slow us down so we hit our move goal more accurately + me->PressCrouchButton(); + } + + if ( fabs( rangeToDispenser - rangeToSentry ) > equalTolerance || rangeToSentry > tooFarRange || rangeToDispenser > tooFarRange ) + { + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, betweenMyBuildings, cost ); + } + + m_path.Update( me ); + } + + if ( rangeToSentry < tooFarRange || rangeToDispenser < tooFarRange ) + { + // we are (nearly) in position - work on our buildings + m_searchTimer.Invalidate(); + + CBaseObject *workTarget = mySentry; + + if ( mySentry->HasSapper() || mySentry->IsPlasmaDisabled() ) + workTarget = mySentry; + else if ( myDispenser->HasSapper() || myDispenser->IsPlasmaDisabled() ) + workTarget = myDispenser; + else if ( mySentry->GetTimeSinceLastInjury() < 1.0f || mySentry->GetHealth() < mySentry->GetMaxHealth() ) + workTarget = mySentry; + else if ( mySentry->IsBuilding() ) + workTarget = mySentry; + else if ( myDispenser->IsBuilding() ) + workTarget = myDispenser; + else if ( mySentry->GetUpgradeLevel() < 3 ) + workTarget = mySentry; + else if ( myDispenser->GetHealth() < myDispenser->GetMaxHealth() ) + workTarget = myDispenser; + else if ( myDispenser->GetUpgradeLevel() < mySentry->GetUpgradeLevel() ) + workTarget = myDispenser; + + me->StopLookingAroundForEnemies(); + me->GetBodyInterface()->AimHeadTowards( workTarget->WorldSpaceCenter(), IBody::CRITICAL, 1.0f, NULL, "Work on my buildings" ); + me->PressFireButton(); + } +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBotEngineerBuilding::IsMetalSourceNearby( CTFBot *me ) const +{ + CUtlVector< CNavArea * > nearbyVector; + CollectSurroundingAreas( &nearbyVector, me->GetLastKnownArea(), 2000.0f, me->GetLocomotionInterface()->GetStepHeight(), me->GetLocomotionInterface()->GetStepHeight() ); + + for( int i=0; i<nearbyVector.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)nearbyVector[i]; + if ( area->HasAttributeTF( TF_NAV_HAS_AMMO ) ) + { + return true; + } + + // this assumes all spawn rooms have resupply cabinets + if ( me->GetTeamNumber() == TF_TEAM_RED && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) ) + { + return true; + } + + if ( me->GetTeamNumber() == TF_TEAM_BLUE && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) + { + return true; + } + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBotEngineerBuilding::CheckIfSentryIsOutOfPosition( CTFBot *me ) const +{ + CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN ); + + if ( !mySentry ) + { + return false; + } + + // payload + if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT ) + { + CTeamTrainWatcher *trainWatcher; + + if ( me->GetTeamNumber() == TF_TEAM_BLUE ) + { + trainWatcher = TFGameRules()->GetPayloadToPush( me->GetTeamNumber() ); + } + else + { + trainWatcher = TFGameRules()->GetPayloadToBlock( me->GetTeamNumber() ); + } + + if ( trainWatcher ) + { + float sentryDistanceAlongPath; + trainWatcher->ProjectPointOntoPath( mySentry->GetAbsOrigin(), NULL, &sentryDistanceAlongPath ); + + const float behindTrainTolerance = SENTRY_MAX_RANGE; + return ( trainWatcher->GetTrainDistanceAlongTrack() > sentryDistanceAlongPath + behindTrainTolerance ); + } + } + + // control points + mySentry->UpdateLastKnownArea(); + CNavArea *sentryArea = mySentry->GetLastKnownArea(); + + CTeamControlPoint *point = me->GetMyControlPoint(); + if ( point ) + { + CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() ); + + if ( sentryArea && pointArea ) + { + CTFBotPathCost cost( me, FASTEST_ROUTE ); + if ( NavAreaTravelDistance( sentryArea, pointArea, cost, tf_bot_engineer_max_sentry_travel_distance_to_point.GetFloat() ) < 0 && + NavAreaTravelDistance( pointArea, sentryArea, cost, tf_bot_engineer_max_sentry_travel_distance_to_point.GetFloat() ) < 0 ) + { + return true; + } + } + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuilding::Update( CTFBot *me, float interval ) +{ + CObjectSentrygun *mySentry = (CObjectSentrygun *)me->GetObjectOfType( OBJ_SENTRYGUN ); + CObjectDispenser *myDispenser = (CObjectDispenser *)me->GetObjectOfType( OBJ_DISPENSER ); + CObjectTeleporter *myTeleportEntrance = (CObjectTeleporter *)me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_ENTRANCE ); + CObjectTeleporter *myTeleportExit = (CObjectTeleporter *)me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT ); + + bool isUnderAttack = ( me->GetTimeSinceLastInjury() < 1.0f ); + isUnderAttack |= ( mySentry && ( mySentry->HasSapper() || mySentry->IsPlasmaDisabled() ) ); + isUnderAttack |= ( myDispenser && ( myDispenser->HasSapper() || myDispenser->IsPlasmaDisabled() ) ); + + me->StartLookingAroundForEnemies(); + + // try to build a Sentry + if ( !mySentry ) + { + m_nearbyMetalStatus = NEARBY_METAL_UNKNOWN; + + // react to nearby threats if our sentry is down + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + me->EquipBestWeaponForThreat( threat ); + } + + if ( !m_hasBuiltSentry && m_sentryTriesLeft > 0 ) + { + --m_sentryTriesLeft; + + if ( m_sentryBuildHint ) + { + return SuspendFor( new CTFBotEngineerBuildSentryGun( m_sentryBuildHint ), "Building a Sentry at a hint location" ); + } + + return SuspendFor( new CTFBotEngineerBuildSentryGun, "Building a Sentry" ); + } + else + { + // can't build a Sentry here - pick a new place + return ChangeTo( new CTFBotEngineerMoveToBuild, "Couldn't find a place to build" ); + } + } + + // I have a Sentry + m_hasBuiltSentry = true; + + if ( m_sentryBuildHint != NULL && !m_sentryBuildHint->IsEnabled() ) + { + // our hint has been disabled and no longer has influence on our behavior + m_sentryBuildHint = NULL; + } + + // periodically check that our Sentry is still near the contested point + if ( m_sentryBuildHint == NULL || !m_sentryBuildHint->IsSticky() ) + { + if ( !m_isSentryOutOfPosition && m_territoryRangeTimer.IsElapsed() ) + { + m_territoryRangeTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + + m_isSentryOutOfPosition = CheckIfSentryIsOutOfPosition( me ); + } + + m_isSentryOutOfPosition = false; + + if ( m_isSentryOutOfPosition ) + { + // the point has moved, only keep sentry as long as it keeps attacking + if ( mySentry->GetTimeSinceLastFired() > 10.0f ) + { + mySentry->DetonateObject(); + + // if we built here because of a hint, disable that hint so we don't use it and rebuild here again + if ( m_sentryBuildHint != NULL ) + { + inputdata_t dummy; + m_sentryBuildHint->InputDisable( dummy ); + + m_sentryBuildHint = NULL; + } + + if ( myDispenser ) + { + myDispenser->DetonateObject(); + } + + if ( myTeleportExit ) + { + myTeleportExit->DetonateObject(); + } + + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_MOVEUP ); + + return ChangeTo( new CTFBotEngineerMoveToBuild, "Need to move my gear closer to the point!" ); + } + } + } + + // if my dispenser is too far away from my sentry, destroy and rebuild it next update + // @TODO: Flag hint-built entities for a larger range + const float maxSeparation = 500.0f; + if ( myDispenser ) + { + if ( ( mySentry->GetAbsOrigin() - myDispenser->GetAbsOrigin() ).IsLengthGreaterThan( maxSeparation ) ) + { + myDispenser->DestroyObject(); + myDispenser = NULL; + } + } + + // build up the sentry all the way if there is a metal source nearby + if ( mySentry->GetUpgradeLevel() < 3 ) + { + if ( m_nearbyMetalStatus == NEARBY_METAL_UNKNOWN ) + { + m_nearbyMetalStatus = IsMetalSourceNearby( me ) ? NEARBY_METAL_EXISTS : NEARBY_METAL_NONE; + } + + if ( m_nearbyMetalStatus == NEARBY_METAL_EXISTS ) + { + UpgradeAndMaintainBuildings( me ); + return Continue(); + } + } + +/* + if ( myTeleportExit ) + { + // if my teleporter exit is too far away from my sentry, destroy and rebuild it next update + if ( ( mySentry->GetAbsOrigin() - myTeleportExit->GetAbsOrigin() ).IsLengthGreaterThan( maxSeparation ) ) + { + myTeleportExit->DestroyObject(); + myTeleportExit = NULL; + } + } +*/ + + // try to build a Dispenser (build after tele exit in training) + if ( !TFGameRules()->IsInTraining() || myTeleportExit ) + { + const float dispenserRebuildInterval = 10.0f; + if ( myDispenser ) + { + // don't rebuild immediately after building is destroyed + m_dispenserRetryTimer.Start( dispenserRebuildInterval ); + } + else if ( m_dispenserRetryTimer.IsElapsed() && !isUnderAttack ) + { + m_dispenserRetryTimer.Start( dispenserRebuildInterval ); + + return SuspendFor( new CTFBotEngineerBuildDispenser, "Building a Dispenser" ); + } + } + + // try to build a Teleporter Exit + const float exitRebuildInterval = TFGameRules()->IsInTraining() ? 5.0f : 30.0f; + if ( myTeleportExit ) + { + // don't rebuild immediately after building is destroyed + m_teleportExitRetryTimer.Start( exitRebuildInterval ); + } + else if ( m_teleportExitRetryTimer.IsElapsed() && myTeleportEntrance && !isUnderAttack ) + { + m_teleportExitRetryTimer.Start( exitRebuildInterval ); + + // we need to build a teleporter exit yet + if ( m_sentryBuildHint != NULL ) + { + // if there are teleporter exit hints, find the closest one to our sentry and use it + CUtlVector< CBaseEntity * > hintVector; + CTFBotHintTeleporterExit *hint = NULL; + while( ( hint = (CTFBotHintTeleporterExit *)gEntList.FindEntityByClassname( hint, "bot_hint_teleporter_exit" ) ) != NULL ) + { + if ( hint->IsEnabled() && hint->InSameTeam( me ) ) + { + hintVector.AddToTail( hint ); + } + } + + if ( hintVector.Count() > 0 ) + { + mySentry->UpdateLastKnownArea(); + CBaseEntity *closeHint = SelectClosestEntityByTravelDistance( me, hintVector, mySentry->GetLastKnownArea(), tf_bot_engineer_exit_near_sentry_range.GetFloat() ); + + if ( closeHint ) + { + return SuspendFor( new CTFBotEngineerBuildTeleportExit( closeHint->GetAbsOrigin(), closeHint->GetAbsAngles().y ), "Building teleporter exit at nearby hint" ); + } + } + } + else if ( me->IsRangeLessThan( mySentry, 300.0f ) ) + { + // drop a teleporter exit near our sentry + return SuspendFor( new CTFBotEngineerBuildTeleportExit(), "Building teleporter exit" ); + } + } + + // everything is built - maintain them + UpgradeAndMaintainBuildings( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotEngineerBuilding::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + me->StartLookingAroundForEnemies(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerBuilding::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEngineerBuilding::OnTerritoryLost( CTFBot *me, int territoryID ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEngineerBuilding::OnTerritoryCaptured( CTFBot *me, int territoryID ) +{ + return TryContinue(); +} diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.h new file mode 100644 index 0000000..e5928ce --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_building.h @@ -0,0 +1,64 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_building.h +// At building location, constructing buildings +// Michael Booth, May 2010 + +#ifndef TF_BOT_ENGINEER_BUILDING_H +#define TF_BOT_ENGINEER_BUILDING_H + +class CTFBotHintSentrygun; + + +class CTFBotEngineerBuilding : public Action< CTFBot > +{ +public: + CTFBotEngineerBuilding( void ); + CTFBotEngineerBuilding( CTFBotHintSentrygun *sentryBuildHint ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID ); + + virtual const char *GetName( void ) const { return "EngineerBuilding"; }; + +private: + CountdownTimer m_searchTimer; + CountdownTimer m_getAmmoTimer; + CountdownTimer m_repathTimer; + CountdownTimer m_buildTeleporterExitTimer; + + int m_sentryTriesLeft; + + CountdownTimer m_dispenserRetryTimer; + CountdownTimer m_teleportExitRetryTimer; + + PathFollower m_path; + + CHandle< CTFBotHintSentrygun > m_sentryBuildHint; + + bool m_hasBuiltSentry; + + enum NearbyMetalType + { + NEARBY_METAL_UNKNOWN, + NEARBY_METAL_NONE, + NEARBY_METAL_EXISTS + }; + + NearbyMetalType m_nearbyMetalStatus; + + CountdownTimer m_territoryRangeTimer; + bool m_isSentryOutOfPosition; + bool CheckIfSentryIsOutOfPosition( CTFBot *me ) const; + + void UpgradeAndMaintainBuildings( CTFBot *me ); + bool IsMetalSourceNearby( CTFBot *me ) const; +}; + + +#endif // TF_BOT_ENGINEER_BUILDING_H diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.cpp b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.cpp new file mode 100644 index 0000000..4ef28d6 --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.cpp @@ -0,0 +1,504 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_move_to_build.cpp +// Engineer moving into position to build +// Michael Booth, February 2009 + +#include "cbase.h" +#include "nav_mesh/tf_nav_mesh.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_obj_sentrygun.h" +#include "tf_weapon_builder.h" +#include "team_train_watcher.h" +#include "bot/tf_bot.h" +#include "bot/behavior/engineer/tf_bot_engineer_build.h" +#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h" +#include "bot/behavior/engineer/tf_bot_engineer_building.h" +#include "bot/map_entities/tf_bot_hint_sentrygun.h" +#include "bot/behavior/tf_bot_get_ammo.h" +#include "bot/behavior/tf_bot_retreat_to_cover.h" +#include "bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h" +#include "trigger_area_capture.h" + +#include "raid/tf_raid_logic.h" + + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_debug_sentry_placement( "tf_bot_debug_sentry_placement", "0", FCVAR_CHEAT ); +ConVar tf_bot_max_teleport_exit_travel_to_point( "tf_bot_max_teleport_exit_travel_to_point", "2500", FCVAR_CHEAT, "In an offensive engineer bot's tele exit is farther from the point than this, destroy it" ); +ConVar tf_bot_min_teleport_travel( "tf_bot_min_teleport_travel", "3000", FCVAR_CHEAT, "Minimum travel distance between teleporter entrance and exit before engineer bot will build one" ); + +//-------------------------------------------------------------------------------------------------------- +static Vector s_pointCentroid; + +int CompareRangeToPoint( CTFNavArea * const *area1, CTFNavArea * const *area2 ) +{ + float d1 = ( (*area1)->GetCenter() - s_pointCentroid ).LengthSqr(); + float d2 = ( (*area2)->GetCenter() - s_pointCentroid ).LengthSqr(); + + // reversed so farthest is sorted first in the vector + if ( d1 < d2 ) + return 1; + + if ( d1 > d2 ) + return -1; + + return 0; +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotEngineerMoveToBuild::CollectBuildAreas( CTFBot *me ) +{ + // if we have a predesignated build area, we're done + if ( me->GetHomeArea() ) + return; + + m_sentryAreaVector.RemoveAll(); + + CUtlVector< CTFNavArea * > pointAreaVector; + Vector pointCentroid = vec3_origin; + float pointEnemyIncursion = 0.0f; + int i; + + int myTeam = me->GetTeamNumber(); + int enemyTeam = ( myTeam == TF_TEAM_BLUE ) ? TF_TEAM_RED : TF_TEAM_BLUE; + + CCaptureZone *zone = me->GetFlagCaptureZone(); + if ( zone ) + { + // NOTE: Not strictly the right thing - should defend location of our team's flag + CTFNavArea *zoneArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( zone->WorldSpaceCenter(), false, 500.0f, true ); + if ( zoneArea ) + { + pointAreaVector.AddToTail( zoneArea ); + pointCentroid += zoneArea->GetCenter(); + pointEnemyIncursion += zoneArea->GetIncursionDistance( enemyTeam ); + } + } + else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT ) + { + CTeamTrainWatcher *trainWatcher; + + if ( myTeam == TF_TEAM_BLUE ) + { + trainWatcher = TFGameRules()->GetPayloadToPush( me->GetTeamNumber() ); + } + else + { + trainWatcher = TFGameRules()->GetPayloadToBlock( me->GetTeamNumber() ); + } + + if ( trainWatcher ) + { + Vector checkpointPos = trainWatcher->GetNextCheckpointPosition(); + + CTFNavArea *checkpointArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( checkpointPos, false, 500.0f, true ); + if ( checkpointArea ) + { + pointAreaVector.AddToTail( checkpointArea ); + pointCentroid += checkpointArea->GetCenter(); + pointEnemyIncursion += checkpointArea->GetIncursionDistance( enemyTeam ); + } + } + } + else + { + // collect all areas overlapping the point + CTeamControlPoint *ctrlPoint = me->GetMyControlPoint(); + if ( !ctrlPoint ) + return; + + const CUtlVector< CTFNavArea * > *ctrlPointAreaVector = TheTFNavMesh()->GetControlPointAreas( ctrlPoint->GetPointIndex() ); + + if ( ctrlPointAreaVector ) + { + for( i=0; i<ctrlPointAreaVector->Count(); ++i ) + { + CTFNavArea *area = ctrlPointAreaVector->Element(i); + + pointAreaVector.AddToTail( area ); + pointCentroid += area->GetCenter(); + pointEnemyIncursion += area->GetIncursionDistance( enemyTeam ); + } + } + } + + if ( pointAreaVector.Count() == 0 ) + return; + + pointCentroid /= pointAreaVector.Count(); + pointEnemyIncursion /= pointAreaVector.Count(); + + + // collect all areas that can see the point + CUtlVector< CTFNavArea * > exposedAreaVector; + for( i=0; i<pointAreaVector.Count(); ++i ) + { + CTFAreaCollector collect; + pointAreaVector[i]->ForAllPotentiallyVisibleAreas( collect ); + + for( int j=0; j<collect.m_vector.Count(); ++j ) + { + CTFNavArea *visibleArea = collect.m_vector[j]; + + + if ( visibleArea->GetIncursionDistance( myTeam ) < 0 || visibleArea->GetIncursionDistance( enemyTeam ) < 0 ) + continue; + + if ( TFGameRules()->IsInKothMode() ) + { + // ignore areas the enemy can reach first + if ( visibleArea->GetIncursionDistance( myTeam ) >= visibleArea->GetIncursionDistance( enemyTeam ) ) + continue; + } + +// incursion flow is badly behaved at cap #1, stage #2 in dustbowl +// else +// { +// if ( pointEnemyIncursion > visibleArea->GetIncursionDistance( enemyTeam ) ) +// continue; +// } + + if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP ) + { + // don't build directly on the point + if ( visibleArea->HasAttributeTF( TF_NAV_CONTROL_POINT ) ) + continue; + + // ignore areas below the point + const float tooFarBelow = 150.0f; + if ( visibleArea->GetCenter().z < pointCentroid.z - tooFarBelow ) + continue; + + // ignore areas too far from the point for the sentry gun to reach + const float tolerance = 1.1f; + if ( ( visibleArea->GetCenter() - pointCentroid ).IsLengthGreaterThan( SENTRY_MAX_RANGE * tolerance ) ) + continue; + } + + // ignore areas that don't have clear line of FIRE (not sight) + const float sentryEyeHeight = 60.0f; + const float pointFlagHeight = 70.0f; // 100.0f; + if ( !me->IsLineOfFireClear( visibleArea->GetCenter() + Vector( 0, 0, sentryEyeHeight ), pointCentroid + Vector( 0, 0, pointFlagHeight ) ) ) + continue; + + if ( !exposedAreaVector.HasElement( visibleArea ) ) + exposedAreaVector.AddToTail( visibleArea ); + } + } + + // keep the farthest away areas + const float keepRatio = 1.0f; // 0.5f; + s_pointCentroid = pointCentroid; + exposedAreaVector.Sort( CompareRangeToPoint ); + + for( i=0; i<exposedAreaVector.Count() * keepRatio; ++i ) + { + CTFNavArea *usableArea = exposedAreaVector[i]; + + m_sentryAreaVector.AddToTail( usableArea ); + } + + // calculate total surface area + m_totalSurfaceArea = 0.0f; + FOR_EACH_VEC( m_sentryAreaVector, it ) + { + CTFNavArea *area = m_sentryAreaVector[ it ]; + + m_totalSurfaceArea += area->GetSizeX() * area->GetSizeY(); + + if ( tf_bot_debug_sentry_placement.GetBool() ) + { + TheNavMesh->AddToSelectedSet( area ); + } + } +} + + +//--------------------------------------------------------------------------------------------- +/** + * Doesn't recompute the potential areas, just reselected from the list + */ +void CTFBotEngineerMoveToBuild::SelectBuildLocation( CTFBot *me ) +{ + m_path.Invalidate(); + + m_sentryBuildHint = NULL; + m_sentryBuildLocation = vec3_origin; + + + // if we have a build spot, use it + if ( me->GetHomeArea() ) + { + m_sentryBuildLocation = me->GetHomeArea()->GetCenter(); + return; + } + + // if we have a set of specific build locations, pick one of them + CUtlVector< CTFBotHintSentrygun * > sentryHintVector; + + CTFBotHintSentrygun *sentryHint; + for( sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( NULL, "bot_hint_sentrygun" ) ); + sentryHint; + sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( sentryHint, "bot_hint_sentrygun" ) ) ) + { + // clear the previous owner if it is us + if ( sentryHint->GetPlayerOwner() == me ) + { + sentryHint->SetPlayerOwner( NULL ); + } + if ( sentryHint->IsAvailableForSelection( me ) ) + { + sentryHintVector.AddToTail( sentryHint ); + } + } + + if ( sentryHintVector.Count() > 0 ) + { + int which = RandomInt( 0, sentryHintVector.Count()-1 ); + + m_sentryBuildHint = sentryHintVector[ which ]; + m_sentryBuildHint->SetPlayerOwner( me ); + m_sentryBuildLocation = m_sentryBuildHint->GetAbsOrigin(); + + return; + } + + + // collect nav area candidates + CollectBuildAreas( me ); + + // choose based on surface area to avoid biasing finely subdivided areas of the mesh + float which = RandomFloat( 0.0f, m_totalSurfaceArea - 1.0f ); + float soFar = 0.0f; + FOR_EACH_VEC( m_sentryAreaVector, sit ) + { + CTFNavArea *area = m_sentryAreaVector[ sit ]; + + soFar += area->GetSizeX() * area->GetSizeY(); + + if ( which < soFar ) + { + m_sentryBuildLocation = area->GetRandomPoint(); + return; + } + } + + if ( !HushAsserts() ) + { + Assert( !"Failed to find a build location" ); + } + m_sentryBuildLocation = me->GetAbsOrigin(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerMoveToBuild::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() ) + { + if ( me->GetHomeArea() && TFGameRules()->GetRaidLogic() ) + { + // try to pick a new area + CTFNavArea *sentryArea = TFGameRules()->GetRaidLogic()->SelectRaidSentryArea(); + if ( sentryArea ) + { + me->SetHomeArea( sentryArea ); + } + } + } +#endif // TF_RAID_MODE + + SelectBuildLocation( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEngineerMoveToBuild::Update( CTFBot *me, float interval ) +{ + if ( me->WasPointJustLost() ) + { + if ( m_fallBackTimer.HasStarted() ) + { + if ( m_fallBackTimer.IsElapsed() ) + { + SelectBuildLocation( me ); + m_fallBackTimer.Invalidate(); + } + else + { + // wait a moment while we decide where to build near fallback point + return Continue(); + } + } + } + + CBaseObject *mySentry = me->GetObjectOfType( OBJ_SENTRYGUN ); + if ( mySentry ) + { + // we already have a sentry from a previous life - continue what we were doing + + // if we used a sentry hint last time, reuse it + CTFBotHintSentrygun *sentryHint; + for( sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( NULL, "bot_hint_sentrygun" ) ); + sentryHint; + sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( sentryHint, "bot_hint_sentrygun" ) ) ) + { + if ( sentryHint->GetPlayerOwner() == me ) + { + return ChangeTo( new CTFBotEngineerBuilding( sentryHint ), "Going back to my existing sentry nest and reusing a sentry hint" ); + } + } + + return ChangeTo( new CTFBotEngineerBuilding, "Going back to my existing sentry nest" ); + } + + // offensive engineers need to place a forward teleporter + if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP && !TFGameRules()->IsInKothMode() && me->GetTeamNumber() == TF_TEAM_BLUE ) + { + CObjectTeleporter *myTeleportExit = (CObjectTeleporter *)me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT ); + int myTeam = me->GetTeamNumber(); + + if ( myTeleportExit ) + { + // if exit is too far from the point, destroy it and try again + CTeamControlPoint *point = me->GetMyControlPoint(); + if ( point ) + { + CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() ); + + myTeleportExit->UpdateLastKnownArea(); + CTFNavArea *exitArea = (CTFNavArea *)myTeleportExit->GetLastKnownArea(); + + if ( pointArea && exitArea ) + { + float travelToPoint = fabs( exitArea->GetIncursionDistance( myTeam ) - pointArea->GetIncursionDistance( myTeam ) ); + + if ( travelToPoint > tf_bot_max_teleport_exit_travel_to_point.GetFloat() ) + { + // too far, destroy it + myTeleportExit->DestroyObject(); + myTeleportExit = NULL; + } + } + } + } + else + { + CObjectTeleporter *myTeleportEntrance = (CObjectTeleporter *)me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_ENTRANCE ); + CTFNavArea *myArea = me->GetLastKnownArea(); + + bool shouldBuildExit = true; + + // if we have a teleporter entrance, don't place the exit too close to it + if ( myTeleportEntrance && myArea ) + { + myTeleportEntrance->UpdateLastKnownArea(); + CTFNavArea *enterArea = (CTFNavArea *)myTeleportEntrance->GetLastKnownArea(); + + if ( enterArea ) + { + float travelBetween = fabs( enterArea->GetIncursionDistance( myTeam ) - myArea->GetIncursionDistance( myTeam ) ); + + if ( travelBetween < tf_bot_min_teleport_travel.GetFloat() ) + { + shouldBuildExit = false; + } + } + } + + if ( shouldBuildExit ) + { + // no exit yet - need to place one + // when we see the enemy, retreat to cover and build the exit there + if ( me->GetVisionInterface()->GetPrimaryKnownThreat( true ) ) + { + if ( !me->m_Shared.InCond( TF_COND_INVULNERABLE ) && ShouldRetreat( me ) != ANSWER_NO ) + { + Action< CTFBot > *nextActionWhenInCover = new CTFBotEngineerBuildTeleportExit; + return SuspendFor( new CTFBotRetreatToCover( nextActionWhenInCover ), "Retreating to a safe place to build my teleporter exit" ); + } + } + } + } + } + + // move to build position + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_path.Compute( me, m_sentryBuildLocation, cost ); + } + + Vector forward; + me->EyeVectors( &forward ); + forward.z = 0.0f; + forward.NormalizeInPlace(); + + Vector myBlueprintPosition = me->GetAbsOrigin() + 50.0f * forward; + + const float closeToHome = 25.0f; + Vector toBuild = m_sentryBuildLocation - myBlueprintPosition; + Vector toMe = m_sentryBuildLocation - me->GetAbsOrigin(); + + if ( me->GetLocomotionInterface()->IsOnGround() ) + { + // we need to wait until we're on the ground since the Build action assumes our position OnStart is where we are going to build + if ( toMe.AsVector2D().IsLengthLessThan( closeToHome ) || toBuild.AsVector2D().IsLengthLessThan( closeToHome ) ) + { + if ( m_sentryBuildHint != NULL ) + { + return ChangeTo( new CTFBotEngineerBuilding( m_sentryBuildHint ), "Reached my precise build location" ); + } + + return ChangeTo( new CTFBotEngineerBuilding, "Reached my build location" ); + } + + m_path.Update( me ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnStuck( CTFBot *me ) +{ +// SelectBuildLocation( me ); + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + SelectBuildLocation( me ); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnTerritoryLost( CTFBot *me, int territoryID ) +{ + // we have to wait a moment until contested point changes to select a new build spot + m_fallBackTimer.Start( 0.2f ); + + return TryContinue(); +} diff --git a/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.h b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.h new file mode 100644 index 0000000..42f60c4 --- /dev/null +++ b/game/server/tf/bot/behavior/engineer/tf_bot_engineer_move_to_build.h @@ -0,0 +1,43 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_engineer_move_to_build.h +// Engineer moving into position to build +// Michael Booth, February 2009 + +#ifndef TF_BOT_ENGINEER_MOVE_TO_BUILD_H +#define TF_BOT_ENGINEER_MOVE_TO_BUILD_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotHintSentrygun; + + +class CTFBotEngineerMoveToBuild : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID ); + + virtual const char *GetName( void ) const { return "EngineerMoveToBuild"; }; + +private: + CHandle< CTFBotHintSentrygun > m_sentryBuildHint; + Vector m_sentryBuildLocation; + + PathFollower m_path; + CountdownTimer m_repathTimer; + + CUtlVector< CTFNavArea * > m_sentryAreaVector; + float m_totalSurfaceArea; + void CollectBuildAreas( CTFBot *me ); + + void SelectBuildLocation( CTFBot *me ); + CountdownTimer m_fallBackTimer; +}; + +#endif // TF_BOT_ENGINEER_MOVE_TO_BUILD_H 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; +} diff --git a/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.h b/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.h new file mode 100644 index 0000000..1ba30e7 --- /dev/null +++ b/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.h @@ -0,0 +1,69 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_medic_heal.h +// Heal a teammate +// Michael Booth, February 2009 + +#ifndef TF_BOT_MEDIC_HEAL_H +#define TF_BOT_MEDIC_HEAL_H + +#include "Path/NextBotChasePath.h" + +class CWeaponMedigun; + +class CTFBotMedicHeal : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + virtual EventDesiredResult< CTFBot > OnActorEmoted( CTFBot *me, CBaseCombatCharacter *emoter, int emote ); + + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + virtual QueryResultType ShouldRetreat( const INextBot *bot ) const; + + virtual const char *GetName( void ) const { return "Heal"; }; + +private: + ChasePath m_chasePath; + + CTFPlayer *SelectPatient( CTFBot *me, CTFPlayer *current ); + CountdownTimer m_changePatientTimer; + + CountdownTimer m_delayUberTimer; + + CHandle< CTFPlayer > m_patient; + Vector m_patientAnchorPos; // a spot where the patient was, to track if they are moving + CountdownTimer m_isPatientRunningTimer; + bool IsPatientRunning( void ) const; + + bool IsStable( CTFPlayer *patient ) const; // return true if the given patient is healthy and safe for now + + CTFNavArea *FindCoverArea( CTFBot *me ); + CTFNavArea *m_coverArea; + CountdownTimer m_coverTimer; + PathFollower m_coverPath; + + void ComputeFollowPosition( CTFBot *me ); + Vector m_followGoal; + + bool IsVisibleToEnemy( CTFBot *me, const Vector &where ) const; + + bool IsReadyToDeployUber( const CWeaponMedigun* pMedigun ) const; + + bool IsGoodUberTarget( CTFPlayer *who ) const; + + bool CanDeployUber( CTFBot *me, const CWeaponMedigun* pMedigun ) const; +}; + +inline bool CTFBotMedicHeal::IsPatientRunning( void ) const +{ + return m_isPatientRunningTimer.IsElapsed() ? false : true; +} + + +#endif // TF_BOT_MEDIC_HEAL_H diff --git a/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.cpp b/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.cpp new file mode 100644 index 0000000..d3d8ed6 --- /dev/null +++ b/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.cpp @@ -0,0 +1,144 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_retreat.cpp +// Retreat towards our spawn to find another patient +// Michael Booth, May 2009 + +#include "cbase.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_retreat.h" + +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; +extern ConVar tf_bot_medic_follow_range; +extern ConVar tf_bot_force_class; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMedicRetreat::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + CTFNavArea *homeArea = me->GetSpawnArea(); + + if ( homeArea == NULL ) + { + return Done( "No home area!" ); + } + + m_path.SetMinLookAheadDistance( tf_bot_path_lookahead_range.GetFloat() ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, homeArea->GetCenter(), cost ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +class CUsefulHealTargetFilter : public INextBotEntityFilter +{ +public: + CUsefulHealTargetFilter( int team ) + { + m_team = team; + } + + virtual bool IsAllowed( CBaseEntity *entity ) const + { + if ( entity && entity->IsPlayer() && entity->GetTeamNumber() == m_team ) + { + return !ToTFPlayer( entity )->IsPlayerClass( TF_CLASS_MEDIC ) && !ToTFPlayer( entity )->IsPlayerClass( TF_CLASS_SNIPER ); + } + return false; + } + + int m_team; +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMedicRetreat::Update( CTFBot *me, float interval ) +{ + // equip the syringegun and defend ourselves! + CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon(); + if ( myWeapon ) + { + if ( myWeapon->GetWeaponID() != TF_WEAPON_SYRINGEGUN_MEDIC ) + { + CBaseCombatWeapon *syringeGun = me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ); + + if ( syringeGun ) + { + me->Weapon_Switch( syringeGun ); + } + } + } + + m_path.Update( me ); + + // look around to try to spot a friend to heal + if ( m_lookAroundTimer.IsElapsed() ) + { + m_lookAroundTimer.Start( RandomFloat( 0.33f, 1.0f ) ); + + QAngle angle; + angle.x = 0.0f; + angle.y = RandomFloat( -180.0f, 180.0f ); + angle.z = 0.0f; + + Vector forward; + AngleVectors( angle, &forward ); + + me->GetBodyInterface()->AimHeadTowards( me->EyePosition() + forward, IBody::IMPORTANT, 0.1f, NULL, "Looking for someone to heal" ); + } + + // if we see a friend, heal them + CUsefulHealTargetFilter filter( me->GetTeamNumber() ); + const CKnownEntity *known = me->GetVisionInterface()->GetClosestKnown( filter ); + if ( known ) + { + return Done( "I know of a teammate" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMedicRetreat::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, me->GetSpawnArea()->GetCenter(), cost ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMedicRetreat::OnStuck( CTFBot *me ) +{ + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, me->GetSpawnArea()->GetCenter(), cost ); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMedicRetreat::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, me->GetSpawnArea()->GetCenter(), cost ); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotMedicRetreat::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + // defend ourselves! + return ANSWER_YES; +} diff --git a/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.h b/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.h new file mode 100644 index 0000000..8ec46a0 --- /dev/null +++ b/game/server/tf/bot/behavior/medic/tf_bot_medic_retreat.h @@ -0,0 +1,29 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_retreat.cpp +// Retreat towards our spawn to find another patient +// Michael Booth, May 2009 + +#ifndef TF_BOT_MEDIC_RETREAT_H +#define TF_BOT_MEDIC_RETREAT_H + +#include "Path/NextBotChasePath.h" + +class CTFBotMedicRetreat : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; + + virtual const char *GetName( void ) const { return "Retreat"; }; + +private: + PathFollower m_path; + CountdownTimer m_lookAroundTimer; +}; + +#endif // TF_BOT_MEDIC_RETREAT_H diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.cpp b/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.cpp new file mode 100644 index 0000000..8f5c8c4 --- /dev/null +++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.cpp @@ -0,0 +1,104 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_mission_destroy_sentries.cpp +// Seek and destroy enemy sentries and ignore everything else +// Michael Booth, June 2011 + +#include "cbase.h" +#include "team.h" +#include "bot/tf_bot.h" +#include "bot/behavior/missions/tf_bot_mission_destroy_sentries.h" +#include "bot/behavior/spy/tf_bot_spy_sap.h" +#include "bot/behavior/tf_bot_destroy_enemy_sentry.h" +#include "bot/behavior/medic/tf_bot_medic_heal.h" +#include "bot/behavior/missions/tf_bot_mission_suicide_bomber.h" +#include "tf_obj_sentrygun.h" + +// +// NOTE: This behavior is deprecated and unused for now. +// The only sentry destroying mission is the Sentry Buster right now (suicide bomber). +// + +//--------------------------------------------------------------------------------------------- +CTFBotMissionDestroySentries::CTFBotMissionDestroySentries( CObjectSentrygun *goalSentry ) +{ + m_goalSentry = goalSentry; +} + + +//--------------------------------------------------------------------------------------------- +CObjectSentrygun *CTFBotMissionDestroySentries::SelectSentryTarget( CTFBot *me ) +{ + + return NULL; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMissionDestroySentries::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + if ( me->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + return ChangeTo( new CTFBotMedicHeal, "My job is to heal/uber the others in the mission" ); + } + + // focus only on the mission + me->SetAttribute( CTFBot::IGNORE_ENEMIES ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMissionDestroySentries::Update( CTFBot *me, float interval ) +{ + if ( m_goalSentry == NULL ) + { + // first destroy the sentry we were assigned to, or any sentry we discovered or that is attacking us + m_goalSentry = me->GetEnemySentry(); + + if ( m_goalSentry == NULL ) + { + // next destroy the most dangerous sentry + int iTeam = ( me->GetTeamNumber() == TF_TEAM_RED ) ? TF_TEAM_BLUE : TF_TEAM_RED; + + if ( TFGameRules() && TFGameRules()->IsPVEModeActive() ) + { + iTeam = TF_TEAM_PVE_DEFENDERS; + } + + m_goalSentry = TFGameRules()->FindSentryGunWithMostKills( iTeam ); + } + } + + // for suicide bombers, we never want them to revert to normal behavior even if there is no sentry to kill + if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + return SuspendFor( new CTFBotMissionSuicideBomber, "On a suicide mission to blow up a sentry" ); + } + + if ( m_goalSentry == NULL ) + { + // no sentries left to destroy - our mission is complete + me->SetMission( CTFBot::NO_MISSION, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM ); + return ChangeTo( GetParentAction()->InitialContainedAction( me ), "Mission complete - reverting to normal behavior" ); + } + + if ( m_goalSentry != me->GetEnemySentry() ) + { + me->RememberEnemySentry( m_goalSentry, m_goalSentry->WorldSpaceCenter() ); + } + + if ( me->IsPlayerClass( TF_CLASS_SPY ) ) + { + return SuspendFor( new CTFBotSpySap( m_goalSentry ), "On a mission to sap a sentry" ); + } + + return SuspendFor( new CTFBotDestroyEnemySentry, "On a mission to destroy a sentry" ); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotMissionDestroySentries::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + me->ClearAttribute( CTFBot::IGNORE_ENEMIES ); +} diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.h b/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.h new file mode 100644 index 0000000..ad07721 --- /dev/null +++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_destroy_sentries.h @@ -0,0 +1,30 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_mission_destroy_sentries.h +// Seek and destroy enemy sentries and ignore everything else +// Michael Booth, June 2011 + +#ifndef TF_BOT_MISSION_DESTROY_SENTRIES_H +#define TF_BOT_MISSION_DESTROY_SENTRIES_H + + +//----------------------------------------------------------------------------- +class CTFBotMissionDestroySentries : public Action< CTFBot > +{ +public: + CTFBotMissionDestroySentries( CObjectSentrygun *goalSentry = NULL ); + virtual ~CTFBotMissionDestroySentries() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual const char *GetName( void ) const { return "MissionDestroySentries"; }; + +private: + CHandle< CObjectSentrygun > m_goalSentry; + + CObjectSentrygun *SelectSentryTarget( CTFBot *me ); +}; + + +#endif // TF_BOT_MISSION_DESTROY_SENTRIES_H diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.cpp b/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.cpp new file mode 100644 index 0000000..07fe474 --- /dev/null +++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.cpp @@ -0,0 +1,377 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_mission_reprogrammed.cpp +// Move to target and explode +// Michael Booth, October 2011 + +#include "cbase.h" +#include "tf_team.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/missions/tf_bot_mission_reprogrammed.h" +#include "particle_parse.h" +#include "tf_obj_sentrygun.h" + +#ifdef STAGING_ONLY + +extern ConVar tf_bot_path_lookahead_range; +extern ConVar tf_bot_suicide_bomb_range; + +ConVar tf_bot_reprogrammed_time( "tf_bot_reprogrammed_time", "8", FCVAR_CHEAT ); + +//--------------------------------------------------------------------------------------------- +CTFBotMissionReprogrammed::CTFBotMissionReprogrammed( void ) +{ +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMissionReprogrammed::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + m_detonateTimer.Invalidate(); + m_detonateSeekTimer.Invalidate(); + m_reprogrammedTimer.Invalidate(); + m_hasDetonated = false; + m_consecutivePathFailures = 0; + m_wasSuccessful = false; + + if ( me->HasTheFlag() ) + me->DropFlag(); + me->SetFlagTarget( NULL ); + + m_victim = me->GetMissionTarget(); + + // Find nearest, accessible teammate + if ( !m_victim ) + { + CTFPlayer *pTarget = FindNearestEnemy( me ); + if ( pTarget ) + { + me->SetMissionTarget( pTarget ); + m_victim = pTarget; + } + } + + if ( m_victim ) + { + m_lastKnownVictimPosition = m_victim->GetAbsOrigin(); + } + + // We're guaranteed to stay alive for a period of time + me->SetHealth( 1 ); + me->m_takedamage = DAMAGE_NO; + + // Duration + m_reprogrammedTimer.Start( tf_bot_reprogrammed_time.GetFloat() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMissionReprogrammed::Update( CTFBot *me, float interval ) +{ + bool bDetonate = false; + + // one we start detonating, there's no turning back + if ( m_detonateTimer.HasStarted() ) + { + if ( m_detonateTimer.IsElapsed() ) + { + m_vecDetLocation = me->GetAbsOrigin(); + Detonate( me ); + + return Done( "KABOOM!" ); + } + + return Continue(); + } + + // Find nearest, accessible teammate + if ( !m_victim || !m_victim->IsAlive() ) + { + m_victim.Set( NULL ); + + CTFPlayer *pTarget = FindNearestEnemy( me ); + if ( pTarget ) + { + me->SetMissionTarget( pTarget ); + m_victim = pTarget; + } + } + + if ( m_victim ) + { + me->m_Shared.RemoveCond( TF_COND_STUNNED ); + + // update chase destination + if ( m_victim->IsAlive() && !m_victim->IsEffectActive( EF_NODRAW ) ) + { + m_lastKnownVictimPosition = m_victim->GetAbsOrigin(); + } + } + + if ( m_talkTimer.IsElapsed() ) + { + m_talkTimer.Start( 4.0f ); + me->EmitSound( "MVM.SentryBusterIntro" ); + } + + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, m_lastKnownVictimPosition, cost ); + + if ( m_path.Compute( me, m_lastKnownVictimPosition, cost ) == false ) + { + ++m_consecutivePathFailures; + + if ( m_consecutivePathFailures >= 3 ) + { + bDetonate = true; + } + } + } + + // move to the victim + m_path.Update( me ); + + // Reprogramming time is up. Find nearest and detonate. + if ( m_reprogrammedTimer.IsElapsed() ) + { + // Limit this mode to 5 seconds + if ( !m_detonateSeekTimer.HasStarted() ) + { + m_detonateSeekTimer.Start( 5.f ); + } + else if ( m_detonateSeekTimer.IsElapsed() ) + { + bDetonate = true; + } + + // Get to a third of the damage range before detonating + const float detonateRange = tf_bot_suicide_bomb_range.GetFloat() / 3.0f; + if ( me->IsDistanceBetweenLessThan( m_lastKnownVictimPosition, detonateRange ) ) + { + if ( me->IsLineOfFireClear( m_lastKnownVictimPosition + Vector( 0, 0, StepHeight ) ) ) + { + bDetonate = true; + } + } + } + + if ( bDetonate ) + { + StartDetonate( me, true ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotMissionReprogrammed::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + if ( me->IsAlive() ) + { + me->ForceChangeTeam( TEAM_SPECTATOR ); + } +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMissionReprogrammed::OnKilled( CTFBot *me, const CTakeDamageInfo &info ) +{ + // Keep us alive, but run to nearest enemy + if ( !m_hasDetonated ) + { + if ( !m_detonateTimer.HasStarted() ) + { + StartDetonate( me ); + } + else + { + Detonate( me ); + } + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMissionReprogrammed::OnStuck( CTFBot *me ) +{ + // we're stuck, decide to detonate now! + if ( !m_hasDetonated && !m_detonateTimer.HasStarted() ) + { + StartDetonate( me ); + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotMissionReprogrammed::StartDetonate( CTFBot *me, bool wasSuccessful /* = false */ ) +{ + if ( m_detonateTimer.HasStarted() ) + return; + + if ( !me->IsAlive() || me->GetHealth() < 1 ) + { + if ( me->GetTeamNumber() != TEAM_SPECTATOR) + { + me->m_lifeState = LIFE_ALIVE; + me->SetHealth( 1 ); + } + } + + m_wasSuccessful = wasSuccessful; + + me->Taunt( TAUNT_BASE_WEAPON ); + m_detonateTimer.Start( 2.f ); + me->EmitSound( "MvM.SentryBusterSpin" ); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotMissionReprogrammed::Detonate( CTFBot *me ) +{ + // BLAST! + m_hasDetonated = true; + + DispatchParticleEffect( "explosionTrail_seeds_mvm", me->GetAbsOrigin(), me->GetAbsAngles() ); + DispatchParticleEffect( "fluidSmokeExpl_ring_mvm", me->GetAbsOrigin(), me->GetAbsAngles() ); + + me->EmitSound( "MVM.SentryBusterExplode" ); + + UTIL_ScreenShake( me->GetAbsOrigin(), 25.0f, 5.0f, 5.0f, 1000.0f, SHAKE_START ); + + if ( !m_wasSuccessful ) + { + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_SENTRY_BUSTER_DOWN, TF_TEAM_PVE_DEFENDERS ); + } + } + + CUtlVector< CTFPlayer* > playerVector; + CUtlVector< CBaseCombatCharacter* > victimVector; + + // Only damage our original team (reprogramming switches team) + CTFTeam *pTeam = me->GetOpposingTFTeam(); + if ( pTeam ) + { + CollectPlayers( &playerVector, pTeam->GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS ); + + // objects + for ( int i = 0; i < pTeam->GetNumObjects(); ++i ) + { + CBaseObject *object = pTeam->GetObject( i ); + if ( object ) + { + victimVector.AddToTail( object ); + } + } + } + + // players + for ( int i = 0; i < playerVector.Count(); ++i ) + { + victimVector.AddToTail( playerVector[i] ); + } + + // non-player bots + CUtlVector< INextBot * > botVector; + TheNextBots().CollectAllBots( &botVector ); + for( int i = 0; i < botVector.Count(); ++i ) + { + CBaseCombatCharacter *bot = botVector[i]->GetEntity(); + + if ( !bot->IsPlayer() && bot->IsAlive() ) + { + victimVector.AddToTail( bot ); + } + } + + // Clear my mission before we have everyone take damage so I will die with the rest + me->SetMission( CTFBot::NO_MISSION, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM ); + me->m_takedamage = DAMAGE_YES; + + // kill victims (including me) + for( int i = 0; i < victimVector.Count(); ++i ) + { + CBaseCombatCharacter *victim = victimVector[i]; + + Vector toVictim = victim->WorldSpaceCenter() - me->WorldSpaceCenter(); + + if ( toVictim.IsLengthGreaterThan( tf_bot_suicide_bomb_range.GetFloat() ) ) + continue; + + if ( victim->IsPlayer() ) + { + color32 colorHit = { 255, 255, 255, 255 }; + UTIL_ScreenFade( victim, colorHit, 1.0f, 0.1f, FFADE_IN ); + } + + if ( me->IsLineOfFireClear( victim ) ) + { + toVictim.NormalizeInPlace(); + + const int nDamage = 1000; + CTakeDamageInfo info( me, me, nDamage, DMG_BLAST, TF_DMG_CUSTOM_NONE ); + + CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 1.0f ); + victim->TakeDamage( info ); + } + } + + // make sure we're removed (in case we detonated in our spawn area where we are invulnerable) + me->ForceChangeTeam( TEAM_SPECTATOR ); +} + +//--------------------------------------------------------------------------------------------- +CTFPlayer *CTFBotMissionReprogrammed::FindNearestEnemy( CTFBot *me ) +{ + CUtlVector< CTFPlayer* > playerVector; + + CollectPlayers( &playerVector, GetEnemyTeam( me->GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS ); + + CTFPlayer *pClosestPlayer = NULL; + float flClosestPlayerDist = FLT_MAX; + + FOR_EACH_VEC( playerVector, i ) + { + if ( !playerVector[i] ) + continue; + + if ( playerVector[i] == me ) + continue; + + me->GetDistanceBetween( playerVector[i] ); + + // Find closest + float flDist = me->GetDistanceBetween( playerVector[i] ); + if ( flDist < flClosestPlayerDist ) + { + pClosestPlayer = playerVector[i]; + flClosestPlayerDist = flDist; + } + } + + return pClosestPlayer; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotMissionReprogrammed::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ( m_detonateTimer.HasStarted() ) ? ANSWER_NO : ANSWER_YES; +} + +#endif // STAGING_ONLY + diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.h b/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.h new file mode 100644 index 0000000..7c63c7e --- /dev/null +++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_reprogrammed.h @@ -0,0 +1,53 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_mission_suicide_bomber.h +// Move to target and explode +// Michael Booth, October 2011 + +#ifndef TF_BOT_MISSION_REPROGRAMMED_H +#define TF_BOT_MISSION_REPROGRAMMED_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotMissionReprogrammed : public Action< CTFBot > +{ +#ifdef STAGING_ONLY +public: + CTFBotMissionReprogrammed( void ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnKilled( CTFBot *me, const CTakeDamageInfo &info ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "MissionReprogrammed"; }; + +private: + CHandle< CBaseEntity > m_victim; // the victim we are trying to destroy + Vector m_lastKnownVictimPosition; + + PathFollower m_path; + CountdownTimer m_repathTimer; + + CountdownTimer m_talkTimer; + CountdownTimer m_detonateTimer; + CountdownTimer m_detonateSeekTimer; + CountdownTimer m_reprogrammedTimer; + + void StartDetonate( CTFBot *me, bool wasSuccessful = false ); + void Detonate( CTFBot *me ); + CTFPlayer *FindNearestEnemy( CTFBot *me ); + bool m_hasDetonated; + bool m_wasSuccessful; + + int m_consecutivePathFailures; + + Vector m_vecDetLocation; +#endif // STAGING_ONLY +}; + + +#endif // TF_BOT_MISSION_REPROGRAMMED_H diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.cpp b/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.cpp new file mode 100644 index 0000000..aac9d6c --- /dev/null +++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.cpp @@ -0,0 +1,400 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_mission_suicide_bomber.cpp +// Move to target and explode +// Michael Booth, October 2011 + +#include "cbase.h" +#include "tf_team.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/missions/tf_bot_mission_suicide_bomber.h" +#include "particle_parse.h" +#include "tf_obj_sentrygun.h" +#include "player_vs_environment/tf_populators.h" + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_suicide_bomb_range( "tf_bot_suicide_bomb_range", "300", FCVAR_CHEAT ); +ConVar tf_bot_suicide_bomb_friendly_fire( "tf_bot_suicide_bomb_friendly_fire", "1", FCVAR_CHEAT ); + +//--------------------------------------------------------------------------------------------- +CTFBotMissionSuicideBomber::CTFBotMissionSuicideBomber( void ) +{ +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMissionSuicideBomber::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + m_detonateTimer.Invalidate(); + m_bHasDetonated = false; + m_consecutivePathFailures = 0; + m_bWasSuccessful = false; + m_bWasKilled = false; + + m_victim = me->GetMissionTarget(); + + if ( m_victim != NULL ) + { + m_lastKnownVictimPosition = m_victim->GetAbsOrigin(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMissionSuicideBomber::Update( CTFBot *me, float interval ) +{ + // one we start detonating, there's no turning back + if ( m_detonateTimer.HasStarted() ) + { + if ( m_detonateTimer.IsElapsed() ) + { + m_vecDetLocation = me->GetAbsOrigin(); + Detonate( me ); + + // Send out an event + if ( m_bWasSuccessful && m_victim && m_victim->IsBaseObject() ) + { + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( m_victim.Get() ); + if ( sentry && sentry->GetOwner() ) + { + CTFPlayer *pOwner = ToTFPlayer( sentry->GetOwner() ); + if ( pOwner ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_sentrybuster_detonate" ); + if ( event ) + { + event->SetInt( "player", pOwner->entindex() ); + event->SetFloat( "det_x", m_vecDetLocation.x ); + event->SetFloat( "det_y", m_vecDetLocation.y ); + event->SetFloat( "det_z", m_vecDetLocation.z ); + gameeventmanager->FireEvent( event ); + } + } + } + } + + return Done( "KABOOM!" ); + } + + return Continue(); + } + + + if ( me->GetHealth() == 1 ) + { + // low on health - detonate where we are! + StartDetonate( me, false, true ); + + return Continue(); + } + + if ( m_victim != NULL ) + { + // update chase destination + if ( m_victim->IsAlive() && !m_victim->IsEffectActive( EF_NODRAW ) ) + { + m_lastKnownVictimPosition = m_victim->GetAbsOrigin(); + } + + // if the engineer is carrying his sentry, he becomes the victim + if ( m_victim->IsBaseObject() ) + { + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( m_victim.Get() ); + if ( sentry && sentry->IsCarried() && sentry->GetOwner() ) + { + // path to the engineer carrying the sentry + m_lastKnownVictimPosition = sentry->GetOwner()->GetAbsOrigin(); + } + } + } + + // Get to a third of the damage range before detonating + const float detonateRange = tf_bot_suicide_bomb_range.GetFloat() / 3.0f; + if ( me->IsDistanceBetweenLessThan( m_lastKnownVictimPosition, detonateRange ) ) + { + if ( me->IsLineOfFireClear( m_lastKnownVictimPosition + Vector( 0, 0, StepHeight ) ) ) + { + StartDetonate( me, true ); + } + } + + if ( m_talkTimer.IsElapsed() ) + { + m_talkTimer.Start( 4.0f ); + me->EmitSound( "MVM.SentryBusterIntro" ); + } + + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + + if ( m_path.Compute( me, m_lastKnownVictimPosition, cost ) == false ) + { + ++m_consecutivePathFailures; + + if ( m_consecutivePathFailures >= 3 ) + { + // really can't reach my victim - detonate! + StartDetonate( me ); + } + } + else + { + m_consecutivePathFailures = 0; + } + } + + // move to the victim + m_path.Update( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotMissionSuicideBomber::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMissionSuicideBomber::OnKilled( CTFBot *me, const CTakeDamageInfo &info ) +{ + if ( !m_bHasDetonated ) + { + if ( !m_detonateTimer.HasStarted() ) + { + StartDetonate( me ); + } + else if ( m_detonateTimer.IsElapsed() ) + { + Detonate( me ); + } + else + { + // We're in detonate mode, and something's trying to kill us. Prevent it. + if ( me->GetTeamNumber() != TEAM_SPECTATOR ) + { + me->m_lifeState = LIFE_ALIVE; + me->SetHealth( 1 ); + } + } + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMissionSuicideBomber::OnStuck( CTFBot *me ) +{ + // we're stuck, decide to detonate now! + if ( !m_bHasDetonated && !m_detonateTimer.HasStarted() ) + { + StartDetonate( me ); + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotMissionSuicideBomber::StartDetonate( CTFBot *me, bool bWasSuccessful /* = false */, bool bWasKilled /*= false*/ ) +{ + if ( m_detonateTimer.HasStarted() ) + return; + + if ( !me->IsAlive() || me->GetHealth() < 1 ) + { + if ( me->GetTeamNumber() != TEAM_SPECTATOR) + { + me->m_lifeState = LIFE_ALIVE; + me->SetHealth( 1 ); + } + } + + m_bWasSuccessful = bWasSuccessful; + m_bWasKilled = bWasKilled; + + me->m_takedamage = DAMAGE_NO; + + me->Taunt( TAUNT_BASE_WEAPON ); + m_detonateTimer.Start( 2.0f ); + me->EmitSound( "MvM.SentryBusterSpin" ); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotMissionSuicideBomber::Detonate( CTFBot *me ) +{ + // BLAST! + m_bHasDetonated = true; + + DispatchParticleEffect( "explosionTrail_seeds_mvm", me->GetAbsOrigin(), me->GetAbsAngles() ); + DispatchParticleEffect( "fluidSmokeExpl_ring_mvm", me->GetAbsOrigin(), me->GetAbsAngles() ); + + me->EmitSound( "MVM.SentryBusterExplode" ); + + UTIL_ScreenShake( me->GetAbsOrigin(), 25.0f, 5.0f, 5.0f, 1000.0f, SHAKE_START ); + + if ( !m_bWasSuccessful ) + { + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_SENTRY_BUSTER_DOWN, TF_TEAM_PVE_DEFENDERS ); + + // ACHIEVEMENT_TF_MVM_KILL_SENTRY_BUSTER + for ( int iDamager = 0 ; iDamager < MAX_ACHIEVEMENT_HISTORY_SLOTS ; iDamager ++ ) + { + EntityHistory_t *damagerHistory = me->m_AchievementData.GetDamagerHistory( iDamager ); + if ( damagerHistory ) + { + if ( damagerHistory->hEntity && ( gpGlobals->curtime - damagerHistory->flTimeDamage <= 5.0f ) ) + { + CTFPlayer *pRecentDamager = ToTFPlayer( damagerHistory->hEntity ); + if ( pRecentDamager ) + { + pRecentDamager->AwardAchievement( ACHIEVEMENT_TF_MVM_KILL_SENTRY_BUSTER ); + } + } + } + } + } + } + + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + CUtlVector< CBaseCombatCharacter * > victimVector; + + int i; + + // players + for ( i=0; i<playerVector.Count(); ++i ) + { + victimVector.AddToTail( playerVector[i] ); + } + + // objects + CTFTeam *team = GetGlobalTFTeam( TF_TEAM_BLUE ); + if ( team ) + { + for ( i=0; i<team->GetNumObjects(); ++i ) + { + CBaseObject *object = team->GetObject( i ); + if ( object ) + { + victimVector.AddToTail( object ); + } + } + } + + team = GetGlobalTFTeam( TF_TEAM_RED ); + if ( team ) + { + for ( i=0; i<team->GetNumObjects(); ++i ) + { + CBaseObject *object = team->GetObject( i ); + if ( object ) + { + victimVector.AddToTail( object ); + } + } + } + + // non-player bots + CUtlVector< INextBot * > botVector; + TheNextBots().CollectAllBots( &botVector ); + for( i=0; i<botVector.Count(); ++i ) + { + CBaseCombatCharacter *bot = botVector[i]->GetEntity(); + + if ( !bot->IsPlayer() && bot->IsAlive() ) + { + victimVector.AddToTail( bot ); + } + } + + // Send out an event whenever players damaged us to the point where we had to detonate + if ( m_bWasKilled ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_sentrybuster_killed" ); + if ( event ) + { + event->SetInt( "sentry_buster", me->entindex() ); + gameeventmanager->FireEvent( event ); + } + } + + // Clear my mission before we have everyone take damage so I will die with the rest + me->SetMission( CTFBot::NO_MISSION, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM ); + me->m_takedamage = DAMAGE_YES; + + // kill victims (including me) + for( int i=0; i<victimVector.Count(); ++i ) + { + CBaseCombatCharacter *victim = victimVector[i]; + + Vector toVictim = victim->WorldSpaceCenter() - me->WorldSpaceCenter(); + + if ( toVictim.IsLengthGreaterThan( tf_bot_suicide_bomb_range.GetFloat() ) ) + continue; + + if ( victim->IsPlayer() ) + { + color32 colorHit = { 255, 255, 255, 255 }; + UTIL_ScreenFade( victim, colorHit, 1.0f, 0.1f, FFADE_IN ); + } + + if ( me->IsLineOfFireClear( victim ) ) + { + toVictim.NormalizeInPlace(); + + int damage = MAX( victim->GetMaxHealth(), victim->GetHealth() ); + + CTakeDamageInfo info( me, me, 4 * damage, DMG_BLAST, TF_DMG_CUSTOM_NONE ); + if ( tf_bot_suicide_bomb_friendly_fire.GetBool() ) + { + info.SetForceFriendlyFire( true ); + } + + CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 1.0f ); + victim->TakeDamage( info ); + } + } + + // make sure we're removed (in case we detonated in our spawn area where we are invulnerable) + me->CommitSuicide( false, true ); + if ( me->IsAlive() ) + { + me->ForceChangeTeam( TEAM_SPECTATOR ); + } + + if ( m_bWasKilled ) + { + // increment num sentry killed this wave + CWave *pWave = g_pPopulationManager ? g_pPopulationManager->GetCurrentWave() : NULL; + if ( pWave ) + { + pWave->IncrementSentryBustersKilled(); + } + } +} + + +// Should we attack "them"? +QueryResultType CTFBotMissionSuicideBomber::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + // buster never "attacks", just approaches and self-detonates + return ANSWER_NO; +} + + diff --git a/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.h b/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.h new file mode 100644 index 0000000..6105bea --- /dev/null +++ b/game/server/tf/bot/behavior/missions/tf_bot_mission_suicide_bomber.h @@ -0,0 +1,49 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_mission_suicide_bomber.h +// Move to target and explode +// Michael Booth, October 2011 + +#ifndef TF_BOT_MISSION_SUICIDE_BOMBER_H +#define TF_BOT_MISSION_SUICIDE_BOMBER_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotMissionSuicideBomber : public Action< CTFBot > +{ +public: + CTFBotMissionSuicideBomber( void ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnKilled( CTFBot *me, const CTakeDamageInfo &info ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "MissionSuicideBomber"; }; + +private: + CHandle< CBaseEntity > m_victim; // the victim we are trying to destroy + Vector m_lastKnownVictimPosition; + + PathFollower m_path; + CountdownTimer m_repathTimer; + + CountdownTimer m_talkTimer; + CountdownTimer m_detonateTimer; + + void StartDetonate( CTFBot *me, bool bWasSuccessful = false, bool bWasKilled = false ); + void Detonate( CTFBot *me ); + bool m_bHasDetonated; + bool m_bWasSuccessful; + bool m_bWasKilled; + + int m_consecutivePathFailures; + + Vector m_vecDetLocation; +}; + + +#endif // TF_BOT_MISSION_SUICIDE_BOMBER_H diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.cpp b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.cpp new file mode 100644 index 0000000..4218c3d --- /dev/null +++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.cpp @@ -0,0 +1,156 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_nav_ent_destroy_entity.h +// Destroy the given entity, under nav entity control +// Michael Booth, September 2009 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.h" + +extern ConVar tf_bot_path_lookahead_range; + +//--------------------------------------------------------------------------------------------- +CTFBotNavEntDestroyEntity::CTFBotNavEntDestroyEntity( const CFuncNavPrerequisite *prereq ) +{ + m_prereq = prereq; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotNavEntDestroyEntity::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + if ( m_prereq == NULL ) + { + return Done( "Prerequisite has been removed before we started" ); + } + + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + m_wasIgnoringEnemies = me->HasAttribute( CTFBot::IGNORE_ENEMIES ); + + m_isReadyToLaunchSticky = true; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotNavEntDestroyEntity::DetonateStickiesWhenSet( CTFBot *me, CTFPipebombLauncher *stickyLauncher ) const +{ + if ( !stickyLauncher ) + return; + + if ( stickyLauncher->GetPipeBombCount() >= 8 || me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 ) + { + // stickies laid - detonate them once they are on the ground + const CUtlVector< CHandle< CTFGrenadePipebombProjectile > > &pipeVector = stickyLauncher->GetPipeBombVector(); + + int i; + for( i=0; i<pipeVector.Count(); ++i ) + { + if ( pipeVector[i].Get() && !pipeVector[i]->m_bTouched ) + { + break; + } + } + + if ( i == pipeVector.Count() ) + { + // stickies are on the ground + me->PressAltFireButton(); + } + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotNavEntDestroyEntity::Update( CTFBot *me, float interval ) +{ + if ( m_prereq == NULL ) + { + return Done( "Prerequisite has been removed" ); + } + + CBaseEntity *target = m_prereq->GetTaskEntity(); + if ( target == NULL ) + { + return Done( "Target entity is NULL" ); + } + + float attackRange = me->GetMaxAttackRange(); + + if ( m_prereq->GetTaskValue() > 0.0f ) + { + attackRange = MIN( attackRange, m_prereq->GetTaskValue() ); + } + + if ( me->IsDistanceBetweenLessThan( target, attackRange ) && me->GetVisionInterface()->IsLineOfSightClearToEntity( target ) ) + { + me->SetAttribute( CTFBot::IGNORE_ENEMIES ); + + me->GetBodyInterface()->AimHeadTowards( target->WorldSpaceCenter(), IBody::CRITICAL, 0.2f, NULL, "Aiming at target we need to destroy to progress" ); + + if ( me->GetBodyInterface()->IsHeadAimingOnTarget() ) + { + // attack + if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + // demomen use stickybombs to destroy the barrier + CTFWeaponBase *myCurrentWeapon = me->m_Shared.GetActiveTFWeapon(); + CTFPipebombLauncher *stickyLauncher = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); + + if ( myCurrentWeapon && myCurrentWeapon->GetWeaponID() != TF_WEAPON_PIPEBOMBLAUNCHER ) + { + me->Weapon_Switch( stickyLauncher ); + } + + if ( m_isReadyToLaunchSticky ) + { + me->PressFireButton(); + } + + m_isReadyToLaunchSticky = !m_isReadyToLaunchSticky; + + DetonateStickiesWhenSet( me, stickyLauncher ); + + return Continue(); + } + + me->EquipBestWeaponForThreat( NULL ); + me->PressFireButton(); + } + + return Continue(); + } + + + if ( !m_wasIgnoringEnemies ) + { + me->ClearAttribute( CTFBot::IGNORE_ENEMIES ); + } + + // move into view of our target + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, target->GetAbsOrigin(), cost ); + } + + m_path.Update( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotNavEntDestroyEntity::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + if ( !m_wasIgnoringEnemies ) + { + me->ClearAttribute( CTFBot::IGNORE_ENEMIES ); + } +} diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.h b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.h new file mode 100644 index 0000000..87dbc15 --- /dev/null +++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.h @@ -0,0 +1,35 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_nav_ent_destroy_entity.h +// Destroy the given entity, under nav entity control +// Michael Booth, September 2009 + +#ifndef TF_BOT_NAV_ENT_DESTROY_ENTITY_H +#define TF_BOT_NAV_ENT_DESTROY_ENTITY_H + +#include "Path/NextBotPathFollow.h" +#include "NextBot/NavMeshEntities/func_nav_prerequisite.h" +#include "tf_weapon_pipebomblauncher.h" + +class CTFBotNavEntDestroyEntity : public Action< CTFBot > +{ +public: + CTFBotNavEntDestroyEntity( const CFuncNavPrerequisite *prereq ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual const char *GetName( void ) const { return "NavEntDestroyEntity"; }; + +private: + CHandle< CFuncNavPrerequisite > m_prereq; + PathFollower m_path; // how we get to the target + CountdownTimer m_repathTimer; + bool m_wasIgnoringEnemies; + + void DetonateStickiesWhenSet( CTFBot *me, CTFPipebombLauncher *stickyLauncher ) const; + bool m_isReadyToLaunchSticky; +}; + + +#endif // TF_BOT_NAV_ENT_DESTROY_ENTITY_H diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.cpp b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.cpp new file mode 100644 index 0000000..22b7f03 --- /dev/null +++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.cpp @@ -0,0 +1,110 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_nav_ent_move_to.cpp +// Move onto target and wait, as directed by nav entity +// Michael Booth, September 2009 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/nav_entities/tf_bot_nav_ent_move_to.h" + +extern ConVar tf_bot_path_lookahead_range; + +//--------------------------------------------------------------------------------------------- +CTFBotNavEntMoveTo::CTFBotNavEntMoveTo( const CFuncNavPrerequisite *prereq ) +{ + m_prereq = prereq; + m_pGoalArea = NULL; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotNavEntMoveTo::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + if ( m_prereq == NULL ) + { + return Done( "Prerequisite has been removed before we started" ); + } + + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + m_waitTimer.Invalidate(); + + CBaseEntity *target = m_prereq->GetTaskEntity(); + if ( target == NULL ) + { + return Done( "Prerequisite target entity is NULL" ); + } + + Extent targetExtent; + targetExtent.Init( target ); + + // pick random ground position within target entity as move-to goal + m_goalPosition = targetExtent.lo + Vector( RandomFloat( 0.0f, targetExtent.SizeX() ), RandomFloat( 0.0f, targetExtent.SizeY() ), targetExtent.SizeZ() ); + + TheNavMesh->GetSimpleGroundHeight( m_goalPosition, &m_goalPosition.z ); + + m_pGoalArea = (CTFNavArea*)TheNavMesh->GetNavArea( m_goalPosition ); + if ( !m_pGoalArea ) + { + return Done( "There's no nav area for the goal position" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotNavEntMoveTo::Update( CTFBot *me, float interval ) +{ + if ( m_prereq == NULL ) + { + return Done( "Prerequisite has been removed" ); + } + + if ( !m_prereq->IsEnabled() ) + { + return Done( "Prerequisite has been disabled" ); + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + // prepare to fight + me->EquipBestWeaponForThreat( threat ); + } + + if ( m_waitTimer.HasStarted() ) + { + if ( m_waitTimer.IsElapsed() ) + { + return Done( "Wait duration elapsed" ); + } + } + else + { + // move to the goal area + if ( m_pGoalArea == me->GetLastKnownArea() ) + { + // in area + m_waitTimer.Start( m_prereq->GetTaskValue() ); + } + else + { + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, m_goalPosition, cost ); + } + + // move into position + m_path.Update( me ); + } + } + + return Continue(); +} + + diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.h b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.h new file mode 100644 index 0000000..53295af --- /dev/null +++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_move_to.h @@ -0,0 +1,35 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_nav_ent_move_to.h +// Move onto target and wait, as directed by nav entity +// Michael Booth, September 2009 + +#ifndef TF_BOT_NAV_ENT_MOVE_TO_H +#define TF_BOT_NAV_ENT_MOVE_TO_H + +#include "Path/NextBotPathFollow.h" +#include "NextBot/NavMeshEntities/func_nav_prerequisite.h" + + +class CTFBotNavEntMoveTo : public Action< CTFBot > +{ +public: + CTFBotNavEntMoveTo( const CFuncNavPrerequisite *prereq ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "NavEntMoveTo"; }; + +private: + CHandle< CFuncNavPrerequisite > m_prereq; + Vector m_goalPosition; // specific position within entity to move to + CTFNavArea* m_pGoalArea; + + CountdownTimer m_waitTimer; + + PathFollower m_path; // how we get to the loot + CountdownTimer m_repathTimer; +}; + + +#endif // TF_BOT_NAV_ENT_MOVE_TO_H diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.cpp b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.cpp new file mode 100644 index 0000000..d55d7fd --- /dev/null +++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.cpp @@ -0,0 +1,56 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_nav_ent_wait.cpp +// Wait for awhile, as directed by nav entity +// Michael Booth, September 2009 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/nav_entities/tf_bot_nav_ent_wait.h" + +extern ConVar tf_bot_path_lookahead_range; + +//--------------------------------------------------------------------------------------------- +CTFBotNavEntWait::CTFBotNavEntWait( const CFuncNavPrerequisite *prereq ) +{ + m_prereq = prereq; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotNavEntWait::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + if ( m_prereq == NULL ) + { + return Done( "Prerequisite has been removed before we started" ); + } + + m_timer.Start( m_prereq->GetTaskValue() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotNavEntWait::Update( CTFBot *me, float interval ) +{ + if ( m_prereq == NULL ) + { + return Done( "Prerequisite has been removed" ); + } + + if ( !m_prereq->IsEnabled() ) + { + return Done( "Prerequisite has been disabled" ); + } + + if ( m_timer.IsElapsed() ) + { + return Done( "Wait time elapsed" ); + } + + return Continue(); +} + + diff --git a/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.h b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.h new file mode 100644 index 0000000..72de0e8 --- /dev/null +++ b/game/server/tf/bot/behavior/nav_entities/tf_bot_nav_ent_wait.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_nav_ent_wait.h +// Wait for awhile, as directed by nav entity +// Michael Booth, September 2009 + +#ifndef TF_BOT_NAV_ENT_WAIT_H +#define TF_BOT_NAV_ENT_WAIT_H + +#include "NextBot/NavMeshEntities/func_nav_prerequisite.h" + + +class CTFBotNavEntWait : public Action< CTFBot > +{ +public: + CTFBotNavEntWait( const CFuncNavPrerequisite *prereq ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "NavEntWait"; }; + +private: + CHandle< CFuncNavPrerequisite > m_prereq; + CountdownTimer m_timer; +}; + + +#endif // TF_BOT_NAV_ENT_WAIT_H diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.cpp b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.cpp new file mode 100644 index 0000000..6d8648b --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.cpp @@ -0,0 +1,231 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_capture_point.cpp +// Move to and try to capture the next point +// Michael Booth, February 2009 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "team_control_point_master.h" +#include "trigger_area_capture.h" +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/capture_point/tf_bot_capture_point.h" +#include "bot/behavior/scenario/capture_point/tf_bot_defend_point.h" +#include "bot/behavior/tf_bot_seek_and_destroy.h" + + +extern ConVar tf_bot_path_lookahead_range; +ConVar tf_bot_offense_must_push_time( "tf_bot_offense_must_push_time", "120", FCVAR_CHEAT, "If timer is less than this, bots will push hard to cap" ); + +ConVar tf_bot_capture_seek_and_destroy_min_duration( "tf_bot_capture_seek_and_destroy_min_duration", "15", FCVAR_CHEAT, "If a capturing bot decides to go hunting, this is the min duration he will hunt for before reconsidering" ); +ConVar tf_bot_capture_seek_and_destroy_max_duration( "tf_bot_capture_seek_and_destroy_max_duration", "30", FCVAR_CHEAT, "If a capturing bot decides to go hunting, this is the max duration he will hunt for before reconsidering" ); + + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotCapturePoint::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + VPROF_BUDGET( "CTFBotCapturePoint::OnStart", "NextBot" ); + + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + m_path.Invalidate(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotCapturePoint::Update( CTFBot *me, float interval ) +{ + if ( TFGameRules()->InSetup() ) + { + // wait until the gates open, then path + m_path.Invalidate(); + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + return Continue(); + } + + CTeamControlPoint *point = me->GetMyControlPoint(); + + if ( point == NULL ) + { + const float roamTime = 10.0f; + return SuspendFor( new CTFBotSeekAndDestroy( roamTime ), "Seek and destroy until a point becomes available" ); + } + + if ( point->GetTeamNumber() == me->GetTeamNumber() ) + { + return ChangeTo( new CTFBotDefendPoint, "We need to defend our point(s)" ); + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + // prepare to fight + me->EquipBestWeaponForThreat( threat ); + } + + bool isPushingToCapture = ( me->IsPointBeingCaptured( point ) && !me->IsInCombat() ) || // a friend is capturing + me->IsCapturingPoint() || // we're capturing + // me->m_Shared.InCond( TF_COND_INVULNERABLE ) || // we're ubered + TFGameRules()->InOvertime() || // the game is in overtime + me->GetTimeLeftToCapture() < tf_bot_offense_must_push_time.GetFloat() || // nearly out of tim + TFGameRules()->IsInTraining() || // teach newbies to capture + me->IsNearPoint( point ); + + + // if we see an enemy at a good combat range, stop and engage them unless we're running out of time + if ( !isPushingToCapture ) + { + if ( threat && threat->IsVisibleRecently() ) + { + return SuspendFor( new CTFBotSeekAndDestroy( RandomFloat( tf_bot_capture_seek_and_destroy_min_duration.GetFloat(), tf_bot_capture_seek_and_destroy_max_duration.GetFloat() ) ), "Too early to capture - hunting" ); + } + } + + + if ( me->IsCapturingPoint() ) + { + // move around on the point while we capture + const CUtlVector< CTFNavArea * > *controlPointAreas = TheTFNavMesh()->GetControlPointAreas( point->GetPointIndex() ); + if ( controlPointAreas ) + { + if ( controlPointAreas->Count() == 0 ) + { + Assert( controlPointAreas->Count() ); + Continue(); // this control point has no nav areas for bot to move around + } + + // move to a random spot on this control point + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + + int which = RandomInt( 0, controlPointAreas->Count() - 1 ); + CTFNavArea *goalArea = controlPointAreas->Element( which ); + if ( goalArea ) + { + CTFBotPathCost cost( me, DEFAULT_ROUTE ); + m_path.Compute( me, goalArea->GetRandomPoint(), cost ); + } + } + + m_path.Update( me ); + } + } + else + { + // move toward the point, periodically repathing to account for changing situation + if ( m_repathTimer.IsElapsed() ) + { + VPROF_BUDGET( "CTFBotCapturePoint::Update( repath )", "NextBot" ); + + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_path.Compute( me, point->GetAbsOrigin(), cost ); + m_repathTimer.Start( RandomFloat( 2.0f, 3.0f ) ); + } + + if ( TFGameRules()->IsInTraining() && !me->IsAnyPointBeingCaptured() ) + { + // stop short of capturing until the human trainee starts it + if ( m_path.GetLength() < 1000.0f ) + { + // hold here and yell at player to get on the point + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_GO ); + + return Continue(); + } + } + + // move towards next capture point + m_path.Update( me ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotCapturePoint::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_repathTimer.Invalidate(); + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotCapturePoint::OnStuck( CTFBot *me ) +{ + m_repathTimer.Invalidate(); + me->GetLocomotionInterface()->ClearStuckStatus(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotCapturePoint::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotCapturePoint::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + m_repathTimer.Invalidate(); + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotCapturePoint::OnTerritoryContested( CTFBot *me, int territoryID ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotCapturePoint::OnTerritoryCaptured( CTFBot *me, int territoryID ) +{ + // we got it, move on + m_repathTimer.Invalidate(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotCapturePoint::OnTerritoryLost( CTFBot *me, int territoryID ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotCapturePoint::ShouldRetreat( const INextBot *bot ) const +{ + CTFBot *me = (CTFBot *)bot->GetEntity(); + + // if we're running out of time, we have to go for it + if ( me->GetTimeLeftToCapture() < tf_bot_offense_must_push_time.GetFloat() ) + return ANSWER_NO; + + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotCapturePoint::ShouldHurry( const INextBot *bot ) const +{ + CTFBot *me = (CTFBot *)bot->GetEntity(); + + // if we're running out of time, we have to go for it + if ( me->GetTimeLeftToCapture() < tf_bot_offense_must_push_time.GetFloat() ) + return ANSWER_YES; + + return ANSWER_UNDEFINED; +} + diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.h b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.h new file mode 100644 index 0000000..af80217 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_capture_point.h @@ -0,0 +1,36 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_capture_point.h +// Move to and try to capture the next point +// Michael Booth, February 2009 + +#ifndef TF_BOT_CAPTURE_POINT_H +#define TF_BOT_CAPTURE_POINT_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotCapturePoint : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID ); + + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + + virtual const char *GetName( void ) const { return "CapturePoint"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; +}; + +#endif // TF_BOT_CAPTURE_POINT_H diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.cpp b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.cpp new file mode 100644 index 0000000..20c090b --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.cpp @@ -0,0 +1,442 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_defend_point.h +// Move to and defend current point from capture +// Michael Booth, February 2009 + +#include "cbase.h" +#include "nav_mesh/tf_nav_mesh.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "team_control_point_master.h" +#include "trigger_area_capture.h" +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/capture_point/tf_bot_defend_point.h" +#include "bot/behavior/scenario/capture_point/tf_bot_capture_point.h" +#include "bot/behavior/medic/tf_bot_medic_heal.h" +#include "bot/behavior/tf_bot_attack.h" +#include "bot/behavior/tf_bot_seek_and_destroy.h" +#include "bot/behavior/engineer/tf_bot_engineer_build.h" +#include "bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.h" +#include "bot/behavior/sniper/tf_bot_sniper_attack.h" +#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h" + + +extern ConVar tf_bot_path_lookahead_range; +extern ConVar tf_bot_min_setup_gate_defend_range; +extern ConVar tf_bot_max_setup_gate_defend_range; +extern ConVar tf_bot_min_setup_gate_sniper_defend_range; +extern ConVar tf_bot_offense_must_push_time; + +ConVar tf_bot_defense_must_defend_time( "tf_bot_defense_must_defend_time", "300", FCVAR_CHEAT, "If timer is less than this, bots will stay near point and guard" ); +ConVar tf_bot_max_point_defend_range( "tf_bot_max_point_defend_range", "1250", FCVAR_CHEAT, "How far (in travel distance) from the point defending bots will take up positions" ); +ConVar tf_bot_defense_debug( "tf_bot_defense_debug", "0", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDefendPoint::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + m_defenseArea = NULL; + + // higher skilled bots prefer to seek and destroy until the time is almost up + static float roamChance[ CTFBot::NUM_DIFFICULTY_LEVELS ] = { 10.0f, 50.0f, 75.0f, 90.0f }; + m_isAllowedToRoam = ( RandomFloat( 0.0f, 100.0f ) < roamChance[ (int)clamp( me->GetDifficulty(), CTFBot::EASY, CTFBot::EXPERT ) ] ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +/** + * Return true if we're in immediate danger of losing the point + */ +bool CTFBotDefendPoint::IsPointThreatened( CTFBot *me ) +{ + CTeamControlPoint *point = me->GetMyControlPoint(); + + if ( point == NULL ) + return false; + + if ( point->LastContestedAt() > 0.0f && ( gpGlobals->curtime - point->LastContestedAt() ) < 5.0f ) + { + // the point is, or was very recently, contested + return true; + } + + // if we just lost a point, we should fall back and stand on the next point to defend against a rush + if ( me->WasPointJustLost() ) + { + return true; + } + +/* + // if an enemy is closer to the point than we are, head them off + // TODO: Compare time to reach, not distance + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat ) + { + const float tolerance = 100.0f; + + float themRange = ( threat->GetLastKnownPosition() - point->GetAbsOrigin() ).Length(); + float myRange = ( me->GetAbsOrigin() - point->GetAbsOrigin() ).Length(); + if ( myRange + tolerance > themRange ) + return true; + } +*/ + + return false; +} + + +//--------------------------------------------------------------------------------------------- +// Are we smart enough to get on the point to block the cap +bool CTFBotDefendPoint::WillBlockCapture( CTFBot *me ) const +{ + if ( TFGameRules()->IsInTraining() ) + return false; + + if ( me->IsDifficulty( CTFBot::EASY ) ) + return false; + + if ( me->IsDifficulty( CTFBot::NORMAL ) ) + { + // 50% chance of blocking cap + return me->TransientlyConsistentRandomValue() > 0.5f; + } + + return true; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDefendPoint::Update( CTFBot *me, float interval ) +{ + // King of the Hill logic + CTeamControlPointMaster *master = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( master && master->GetNumPoints() == 1 ) + { + // if we don't own the only point, switch to capture behavior + CTeamControlPoint *point = master->GetControlPoint( 0 ); + if ( point && point->GetOwner() != me->GetTeamNumber() ) + { + return ChangeTo( new CTFBotCapturePoint, "We need to capture the point!" ); + } + } + + CTeamControlPoint *point = me->GetMyControlPoint(); + + if ( point == NULL ) + { + const float roamTime = 10.0f; + return SuspendFor( new CTFBotSeekAndDestroy( roamTime ), "Seek and destroy until a point becomes available" ); + } + + if ( point->GetTeamNumber() != me->GetTeamNumber() ) + { + return ChangeTo( new CTFBotCapturePoint, "We need to capture our point(s)" ); + } + + // if point in is danger - get ON the point! + // Don't do this in training to keep things easy for the new trainee + if ( IsPointThreatened( me ) && WillBlockCapture( me ) ) + { + // point is being captured - get on it! + return SuspendFor( new CTFBotDefendPointBlockCapture, "Moving to block point capture!" ); + } + + // point is safe for the moment + + // if I'm uber'd, go get 'em! + if ( me->m_Shared.InCond( TF_COND_INVULNERABLE ) ) + { + const float uberChargeTime = 6.0; + return SuspendFor( new CTFBotSeekAndDestroy( uberChargeTime ), "Attacking because I'm uber'd!" ); + } + + if ( point && point->IsLocked() ) + { + return SuspendFor( new CTFBotSeekAndDestroy, "Seek and destroy until the point unlocks" ); + } + + if ( m_isAllowedToRoam && me->GetTimeLeftToCapture() > tf_bot_defense_must_defend_time.GetFloat() ) + { + return SuspendFor( new CTFBotSeekAndDestroy( 15.0f ), "Seek and destroy - we have lots of time" ); + } + + if ( TFGameRules()->InSetup() ) + { + // don't lose patience during setup time + m_idleTimer.Reset(); + } + + // if we see an enemy as we have a melee weapon equipped, chase them down + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + + me->EquipBestWeaponForThreat( threat ); + + if ( threat && threat->IsVisibleRecently() ) + { + // we're aware of an enemy + m_idleTimer.Reset(); + + if ( me->IsPlayerClass( TF_CLASS_PYRO ) ) + { + // go get 'em + return SuspendFor( new CTFBotSeekAndDestroy( 15.0f ), "Going after an enemy" ); + } + + CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon(); + if ( myWeapon && ( myWeapon->IsMeleeWeapon() || myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) ) ) + { + // TODO: Check if threat is visible and if not, move to last known position + CTFBotPathCost cost( me, me->IsPlayerClass( TF_CLASS_PYRO ) ? SAFEST_ROUTE : FASTEST_ROUTE ); + m_chasePath.Update( me, threat->GetEntity(), cost ); + + return Continue(); + } + } + + // choose where we'll defend from + if ( m_defenseArea == NULL || m_idleTimer.IsElapsed() ) + { + m_defenseArea = SelectAreaToDefendFrom( me ); + } + + if ( m_defenseArea ) + { + if ( me->GetLastKnownArea() == m_defenseArea ) + { + // at our defense position + if ( CTFBotPrepareStickybombTrap::IsPossible( me ) ) + { + return SuspendFor( new CTFBotPrepareStickybombTrap, "Laying sticky bombs!" ); + } + } + else + { + // move to our desired defense position, repathing periodically to account for changing situation + VPROF_BUDGET( "CTFBotDefendPoint::Update( repath )", "NextBot" ); + + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 2.0f, 3.0f ) ); + + CTFBotPathCost cost( me, DEFAULT_ROUTE ); + m_path.Compute( me, m_defenseArea->GetCenter(), cost ); + } + + m_path.Update( me ); + + // we're not idle while we're moving to our defend position + m_idleTimer.Reset(); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDefendPoint::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + // may have lost point - recheck + me->ClearMyControlPoint(); + m_repathTimer.Invalidate(); + m_path.Invalidate(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPoint::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPoint::OnStuck( CTFBot *me ) +{ + m_path.Invalidate(); + m_defenseArea = SelectAreaToDefendFrom( me ); + me->GetLocomotionInterface()->ClearStuckStatus(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPoint::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPoint::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + m_path.Invalidate(); + m_defenseArea = SelectAreaToDefendFrom( me ); + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPoint::OnTerritoryContested( CTFBot *me, int territoryID ) +{ + // handled in the Update() loop + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPoint::OnTerritoryCaptured( CTFBot *me, int territoryID ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPoint::OnTerritoryLost( CTFBot *me, int territoryID ) +{ + // we lost it, fall back to next point + me->ClearMyControlPoint(); + m_defenseArea = SelectAreaToDefendFrom( me ); + m_repathTimer.Invalidate(); + m_path.Invalidate(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +class CSelectDefenseAreaForPoint : public ISearchSurroundingAreasFunctor +{ +public: + CSelectDefenseAreaForPoint( CTFNavArea *pointArea, int myTeam, CUtlVector< CTFNavArea * > *areaVector ) + { + m_pointArea = pointArea; + m_myTeam = myTeam; + + // don't select areas that are beyond the point + m_incursionFlowLimit = pointArea->GetIncursionDistance( m_myTeam ) + 250.0f; + + m_areaVector = areaVector; + m_areaVector->RemoveAll(); + } + + virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) + { + CTFNavArea *area = (CTFNavArea *)baseArea; + + if ( !TFGameRules()->IsInKothMode() ) + { + // don't select areas that are beyond the point + if ( area->GetIncursionDistance( m_myTeam ) > m_incursionFlowLimit ) + return true; + } + + if ( area->IsPotentiallyVisible( m_pointArea ) ) + { + // a bit of a hack here to avoid bots choosing to defend in bottom of ravine at stage 3 of dustbowl + const float tooLow = 220.0f; + if ( m_pointArea->GetCenter().z - area->GetCenter().z < tooLow ) + { + // valid defense position + m_areaVector->AddToTail( area ); + } + } + + return true; + } + + virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar ) + { + if ( adjArea->IsBlocked( TFGameRules()->IsInKothMode() ? TEAM_ANY : m_myTeam ) ) + { + return false; + } + + if ( travelDistanceSoFar > tf_bot_max_point_defend_range.GetFloat() ) + { + // too far away + return false; + } + + const float maxHeightChange = 65.0f; + float deltaZ = currentArea->ComputeAdjacentConnectionHeightChange( adjArea ); + return ( fabs( deltaZ ) < maxHeightChange ); + } + + CTFNavArea *m_pointArea; + CUtlVector< CTFNavArea * > *m_areaVector; + float m_incursionFlowLimit; + int m_myTeam; +}; + + +//--------------------------------------------------------------------------------------------- +/** + * Select the area where we will guard the point from + */ +CTFNavArea *CTFBotDefendPoint::SelectAreaToDefendFrom( CTFBot *me ) +{ + VPROF_BUDGET( "CTFBotDefendPoint::SelectAreaToDefendFrom", "NextBot" ); + + CTeamControlPoint *point = me->GetMyControlPoint(); + if ( !point ) + { + return NULL; + } + + // decide where we will defend from + CUtlVector< CTFNavArea * > defenseAreas; + +/* + if ( !TFGameRules()->IsInKothMode() && + point->GetTeamCapPercentage( me->GetTeamNumber() ) <= 0.0f && // point is currently safe + ( ObjectiveResource()->GetPreviousPointForPoint( point->GetPointIndex(), me->GetTeamNumber(), 0 ) < 0 || // this is the first cap point + me->IsPlayerClass( TF_CLASS_PYRO ) ) ) // pyros are skirmishers + { + if ( TheTFNavMesh()->GetSetupGateDefenseAreas() ) + { + defenseAreas = *TheTFNavMesh()->GetSetupGateDefenseAreas(); + } + } +*/ + + if ( defenseAreas.Count() == 0 ) + { + CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() ); + if ( pointArea ) + { + // search outwards from the point along walkable areas (not drop downs) to make sure we can get back to the point quickly + CSelectDefenseAreaForPoint defenseScan( pointArea, me->GetTeamNumber(), &defenseAreas ); + SearchSurroundingAreas( pointArea, defenseScan ); + } + } + + // select a specific area from the potential defense set + if ( defenseAreas.Count() == 0 ) + { + return NULL; + } + + // how long will we wait if we don't see any action + m_idleTimer.Start( RandomFloat( 10.0f, 20.0f ) ); + + if ( tf_bot_defense_debug.GetBool() ) + { + for( int i=0; i<defenseAreas.Count(); ++i ) + { + defenseAreas[i]->DrawFilled( 0, 200, 200, 999.9f ); + } + } + + // select one of the defense areas + int which = RandomInt( 0, defenseAreas.Count()-1 ); + return defenseAreas[ which ]; +} + diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.h b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.h new file mode 100644 index 0000000..c68a55c --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point.h @@ -0,0 +1,48 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_defend_point.h +// Move to and defend current point from capture +// Michael Booth, February 2009 + +#ifndef TF_BOT_DEFEND_POINT_H +#define TF_BOT_DEFEND_POINT_H + +#include "Path/NextBotPathFollow.h" +#include "Path/NextBotChasePath.h" + +class CTFBotDefendPoint : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID ); + + virtual const char *GetName( void ) const { return "DefendPoint"; }; + +private: + PathFollower m_path; // for moving to a defense position + ChasePath m_chasePath; // for chasing enemies + + CountdownTimer m_repathTimer; + CountdownTimer m_lookAroundTimer; + CountdownTimer m_idleTimer; + + CTFNavArea *m_defenseArea; + CTFNavArea *SelectAreaToDefendFrom( CTFBot *me ); + + bool IsPointThreatened( CTFBot *me ); + bool WillBlockCapture( CTFBot *me ) const; + bool m_isAllowedToRoam; +}; + + +#endif // TF_BOT_DEFEND_POINT_H diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.cpp b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.cpp new file mode 100644 index 0000000..6e5bbf7 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.cpp @@ -0,0 +1,247 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_defend_point_block_capture.h +// Move to and defend current point from capture +// Michael Booth, February 2009 + +#include "cbase.h" +#include "nav_mesh/tf_nav_mesh.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "trigger_area_capture.h" +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.h" +#include "bot/behavior/medic/tf_bot_medic_heal.h" +#include "bot/behavior/tf_bot_attack.h" +#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h" + + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_defend_owned_point_percent( "tf_bot_defend_owned_point_percent", "0.5", FCVAR_CHEAT, "Stay on the contested point we own until enemy cap percent falls below this" ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDefendPointBlockCapture::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + m_point = me->GetMyControlPoint(); + if ( m_point == NULL ) + { + return Done( "Point is NULL" ); + } + + m_defenseArea = static_cast< CTFNavArea * >( TheTFNavMesh()->GetNearestNavArea( m_point->GetAbsOrigin() ) ); + if ( m_defenseArea == NULL ) + { + return Done( "Can't find nav area on point" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBotDefendPointBlockCapture::IsPointSafe( CTFBot *me ) +{ + // if a point was just captured, defend this point for awhile + if ( me->WasPointJustLost() ) + { + return false; + } + + if ( m_point == NULL ) + { + return true; + } + + if ( m_point->GetTeamCapPercentage( me->GetTeamNumber() ) < tf_bot_defend_owned_point_percent.GetFloat() ) + { + // we're not in complete control of this point yet + return false; + } + + // is point is being contested, or was just being contested, its not safe + if ( m_point->HasBeenContested() && ( gpGlobals->curtime - m_point->LastContestedAt() ) < 5.0f ) + { + return false; + } + + // if we still see a near threat, stay put + const CKnownEntity *knownThreat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( knownThreat ) + { + const float dangerRange = 500.0f; + if ( ( knownThreat->GetLastKnownPosition() - m_point->GetAbsOrigin() ).IsLengthLessThan( dangerRange ) ) + return false; + } + + return true; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDefendPointBlockCapture::Update( CTFBot *me, float interval ) +{ + // if point is safe, we can move back to our defense positions + if ( IsPointSafe( me ) ) + { + return Done( "Point is safe again" ); + } + + if ( me->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + // medics look ridiculous rushing to the point - they need to heal + return SuspendFor( new CTFBotMedicHeal ); + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + me->EquipBestWeaponForThreat( threat ); + + Extent pointExtent; + pointExtent.Init( m_point ); + + bool isStandingOnThePoint = pointExtent.Contains( me->GetAbsOrigin() ); + + const CUtlVector< CTFNavArea * > *controlPointAreas = TheTFNavMesh()->GetControlPointAreas( m_point->GetPointIndex() ); + if ( controlPointAreas ) + { + for( int i=0; i<controlPointAreas->Count(); ++i ) + { + if ( me->GetLastKnownArea() && me->GetLastKnownArea()->GetID() == controlPointAreas->Element(i)->GetID() ) + { + isStandingOnThePoint = true; + } + } + } + + if ( isStandingOnThePoint && CTFBotPrepareStickybombTrap::IsPossible( me ) ) + { + return SuspendFor( new CTFBotPrepareStickybombTrap, "Placing stickies for defense" ); + } + + if ( controlPointAreas ) + { + // move to a random spot on this control point + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + + float totalArea = 0.0f; + int i; + for( i=0; i<controlPointAreas->Count(); ++i ) + { + CTFNavArea *area = controlPointAreas->Element(i); + totalArea += area->GetSizeX() * area->GetSizeY(); + } + + float which = RandomFloat( 0.0f, totalArea - 1.0f ); + CTFNavArea *goalArea = NULL; + for( i=0; i<controlPointAreas->Count(); ++i ) + { + CTFNavArea *area = controlPointAreas->Element(i); + which -= area->GetSizeX() * area->GetSizeY(); + if ( which <= 0.0f ) + { + goalArea = area; + break; + } + } + + if ( goalArea ) + { + CTFBotPathCost cost( me, DEFAULT_ROUTE ); + m_path.Compute( me, goalArea->GetRandomPoint(), cost ); + } + } + + m_path.Update( me ); + } + else if ( !isStandingOnThePoint ) + { + // get on the point! + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + + CTFBotPathCost cost( me, DEFAULT_ROUTE ); + m_path.Compute( me, ( pointExtent.lo + pointExtent.hi )/2.0f, cost ); + } + + m_path.Update( me ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDefendPointBlockCapture::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_path.Invalidate(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnStuck( CTFBot *me ) +{ + m_path.Invalidate(); + me->GetLocomotionInterface()->ClearStuckStatus(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + m_path.Invalidate(); + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnTerritoryContested( CTFBot *me, int territoryID ) +{ + return TryToSustain(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnTerritoryCaptured( CTFBot *me, int territoryID ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDefendPointBlockCapture::OnTerritoryLost( CTFBot *me, int territoryID ) +{ + // we lost it, fall back + return TryDone( RESULT_CRITICAL, "Lost the point" ); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotDefendPointBlockCapture::ShouldHurry( const INextBot *me ) const +{ + // hurry up and get on the point! + return ANSWER_YES; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotDefendPointBlockCapture::ShouldRetreat( const INextBot *me ) const +{ + // get on the point! + return ANSWER_NO; +} diff --git a/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.h b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.h new file mode 100644 index 0000000..55ab7af --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_point/tf_bot_defend_point_block_capture.h @@ -0,0 +1,40 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_defend_point_block_capture.h +// Move to and defend current point from capture +// Michael Booth, February 2009 + +#ifndef TF_BOT_DEFEND_POINT_BLOCK_CAPTURE_H +#define TF_BOT_DEFEND_POINT_BLOCK_CAPTURE_H + + +class CTFBotDefendPointBlockCapture : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID ); + + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; + + virtual const char *GetName( void ) const { return "BlockCapture"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; + CTeamControlPoint *m_point; + CTFNavArea *m_defenseArea; + + bool IsPointSafe( CTFBot *me ); +}; + + +#endif // TF_BOT_DEFEND_POINT_BLOCK_CAPTURE_H diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.cpp b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.cpp new file mode 100644 index 0000000..95f1480 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.cpp @@ -0,0 +1,126 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_attack_flag_defenders.cpp +// Attack enemies that are preventing the flag from reaching its destination +// Michael Booth, May 2011 + +#include "cbase.h" + +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h" + +ConVar tf_bot_flag_escort_range( "tf_bot_flag_escort_range", "500", FCVAR_CHEAT ); + +extern ConVar tf_bot_flag_escort_max_count; + +extern int GetBotEscortCount( int team ); + + +//--------------------------------------------------------------------------------------------- +CTFBotAttackFlagDefenders::CTFBotAttackFlagDefenders( float minDuration ) +{ + if ( minDuration > 0.0f ) + { + m_minDurationTimer.Start( minDuration ); + } + else + { + m_minDurationTimer.Invalidate(); + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotAttackFlagDefenders::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + m_chasePlayer = NULL; + return CTFBotAttack::OnStart( me, priorAction ); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotAttackFlagDefenders::Update( CTFBot *me, float interval ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + // prepare to fight + me->EquipBestWeaponForThreat( threat ); + } + + if ( m_watchFlagTimer.IsElapsed() && m_minDurationTimer.IsElapsed() ) + { + m_watchFlagTimer.Start( RandomFloat( 1.0f, 3.0f ) ); + + CCaptureFlag *flag = me->GetFlagToFetch(); + + if ( !flag ) + { + return Done( "No flag" ); + } + + // can't reach flag if it is at home + if ( !TFGameRules()->IsMannVsMachineMode() || !flag->IsHome() ) + { + CTFPlayer *carrier = ToTFPlayer( flag->GetOwnerEntity() ); + if ( !carrier ) + { + return Done( "Flag was dropped" ); + } + + if ( me->IsSelf( carrier ) ) + { + return Done( "I picked up the flag!" ); + } + + // escort the flag carrier, unless the carrier is in a squad + CTFBot *botCarrier = ToTFBot( carrier ); + if ( !botCarrier || !botCarrier->IsInASquad() ) + { + if ( me->IsRangeLessThan( carrier, tf_bot_flag_escort_range.GetFloat() ) ) + { + if ( GetBotEscortCount( me->GetTeamNumber() ) < tf_bot_flag_escort_max_count.GetInt() ) + { + return ChangeTo( new CTFBotEscortFlagCarrier, "Near flag carrier - escorting" ); + } + } + } + } + } + + ActionResult< CTFBot > result = CTFBotAttack::Update( me, interval ); + + if ( result.IsDone() ) + { + // nothing to attack, move towards a random player + + if ( m_chasePlayer == NULL || !m_chasePlayer->IsAlive() ) + { + m_chasePlayer = me->SelectRandomReachableEnemy(); + } + + if ( m_chasePlayer == NULL ) + { + // everyone is dead or hiding in the spawn room - go escort the flag + return ChangeTo( new CTFBotEscortFlagCarrier, "No reachable victim - escorting flag" ); + } + + // cheat and "see" our victim so we know where to go + me->GetVisionInterface()->AddKnownEntity( m_chasePlayer ); + + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 3.0f ) ); + + CTFBotPathCost cost( me, DEFAULT_ROUTE ); + float maxPathLength = TFGameRules()->IsMannVsMachineMode() ? TFBOT_MVM_MAX_PATH_LENGTH : 0.0f; + m_path.Compute( me, m_chasePlayer, cost, maxPathLength ); + } + + m_path.Update( me ); + } + + return Continue(); +} diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h new file mode 100644 index 0000000..662ef8a --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h @@ -0,0 +1,34 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_attack_flag_defenders.h +// Attack enemies that are preventing the flag from reaching its destination +// Michael Booth, May 2011 + +#ifndef TF_BOT_ATTACK_FLAG_DEFENDERS_H +#define TF_BOT_ATTACK_FLAG_DEFENDERS_H + +#include "Path/NextBotPathFollow.h" +#include "bot/behavior/tf_bot_attack.h" + + +//----------------------------------------------------------------------------- +class CTFBotAttackFlagDefenders : public CTFBotAttack +{ +public: + CTFBotAttackFlagDefenders( float minDuration = -1.0f ); + virtual ~CTFBotAttackFlagDefenders() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "AttackFlagDefenders"; } + +private: + CountdownTimer m_minDurationTimer; + CountdownTimer m_watchFlagTimer; + CHandle< CTFPlayer > m_chasePlayer; + PathFollower m_path; + CountdownTimer m_repathTimer; +}; + + +#endif // TF_BOT_ATTACK_FLAG_DEFENDERS_H diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.cpp b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.cpp new file mode 100644 index 0000000..873c36e --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.cpp @@ -0,0 +1,422 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_deliver_flag.cpp +// Take the flag we are holding to its destination +// Michael Booth, May 2011 + +#include "cbase.h" + +#include "tf_player_shared.h" + +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h" +#include "bot/behavior/tf_bot_taunt.h" +#include "bot/behavior/tf_bot_mvm_deploy_bomb.h" + +#include "tf_objective_resource.h" +#include "player_vs_environment/tf_population_manager.h" +#include "econ_item_system.h" +#include "tf_gamestats.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 "particle_parse.h" + +ConVar tf_mvm_bot_allow_flag_carrier_to_fight( "tf_mvm_bot_allow_flag_carrier_to_fight", "1", FCVAR_CHEAT ); + +ConVar tf_mvm_bot_flag_carrier_interval_to_1st_upgrade( "tf_mvm_bot_flag_carrier_interval_to_1st_upgrade", "5", FCVAR_CHEAT ); +ConVar tf_mvm_bot_flag_carrier_interval_to_2nd_upgrade( "tf_mvm_bot_flag_carrier_interval_to_2nd_upgrade", "15", FCVAR_CHEAT ); +ConVar tf_mvm_bot_flag_carrier_interval_to_3rd_upgrade( "tf_mvm_bot_flag_carrier_interval_to_3rd_upgrade", "15", FCVAR_CHEAT ); + +ConVar tf_mvm_bot_flag_carrier_health_regen( "tf_mvm_bot_flag_carrier_health_regen", "45.0f", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDeliverFlag::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_flTotalTravelDistance = -1.0f; + + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + if ( tf_mvm_bot_allow_flag_carrier_to_fight.GetBool() == false ) + { + me->SetAttribute( CTFBot::SUPPRESS_FIRE ); + } + + // mini-bosses don't upgrade - they are already tough + if ( me->IsMiniBoss() ) + { + m_upgradeLevel = DONT_UPGRADE; + if ( TFObjectiveResource() ) + { + // Set threat level to max + TFObjectiveResource()->SetFlagCarrierUpgradeLevel( 4 ); + TFObjectiveResource()->SetBaseMvMBombUpgradeTime( -1 ); + TFObjectiveResource()->SetNextMvMBombUpgradeTime( -1 ); + } + } + else + { + m_upgradeLevel = 0; + m_upgradeTimer.Start( tf_mvm_bot_flag_carrier_interval_to_1st_upgrade.GetFloat() ); + if ( TFObjectiveResource() ) + { + TFObjectiveResource()->SetBaseMvMBombUpgradeTime( gpGlobals->curtime ); + TFObjectiveResource()->SetNextMvMBombUpgradeTime( gpGlobals->curtime + m_upgradeTimer.GetRemainingTime() ); + } + + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +// In Mann Vs Machine, the flag carrier gets stronger the longer he carries the flag +bool CTFBotDeliverFlag::UpgradeOverTime( CTFBot *me ) +{ + if ( TFGameRules()->IsMannVsMachineMode() && m_upgradeLevel != DONT_UPGRADE ) + { + 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 ) ) + { + // don't start counting down until we leave the spawn + m_upgradeTimer.Start( tf_mvm_bot_flag_carrier_interval_to_1st_upgrade.GetFloat() ); + TFObjectiveResource()->SetBaseMvMBombUpgradeTime( gpGlobals->curtime ); + TFObjectiveResource()->SetNextMvMBombUpgradeTime( gpGlobals->curtime + m_upgradeTimer.GetRemainingTime() ); + } + + // do defensive buff effect ourselves (since we're not a soldier) + if ( m_upgradeLevel > 0 && m_buffPulseTimer.IsElapsed() ) + { + m_buffPulseTimer.Start( 1.0f ); + + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, me->GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS ); + + const float buffRadius = 450.0f; + + for( int i=0; i<playerVector.Count(); ++i ) + { + if ( me->IsRangeLessThan( playerVector[i], buffRadius ) ) + { + playerVector[i]->m_Shared.AddCond( TF_COND_DEFENSEBUFF_NO_CRIT_BLOCK, 1.2f ); + } + } + } + + // the flag carrier gets stronger the longer he holds the flag + if ( m_upgradeTimer.IsElapsed() ) + { + const int maxLevel = 3; + + if ( m_upgradeLevel < maxLevel ) + { + ++m_upgradeLevel; + + TFGameRules()->BroadcastSound( 255, "MVM.Warning" ); + + switch( m_upgradeLevel ) + { + //--------------------------------------- + case 1: + m_upgradeTimer.Start( tf_mvm_bot_flag_carrier_interval_to_2nd_upgrade.GetFloat() ); + + // permanent buff banner effect (handled above) + + // update the objective resource so clients have the information + if ( TFObjectiveResource() ) + { + TFObjectiveResource()->SetFlagCarrierUpgradeLevel( 1 ); + TFObjectiveResource()->SetBaseMvMBombUpgradeTime( gpGlobals->curtime ); + TFObjectiveResource()->SetNextMvMBombUpgradeTime( gpGlobals->curtime + m_upgradeTimer.GetRemainingTime() ); + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_BOMB_CARRIER_UPGRADE1, TF_TEAM_PVE_DEFENDERS ); + DispatchParticleEffect( "mvm_levelup1", PATTACH_POINT_FOLLOW, me, "head" ); + } + return true; + + //--------------------------------------- + case 2: + { + static CSchemaAttributeDefHandle pAttrDef_HealthRegen( "health regen" ); + + m_upgradeTimer.Start( tf_mvm_bot_flag_carrier_interval_to_3rd_upgrade.GetFloat() ); + + if ( !pAttrDef_HealthRegen ) + { + Warning( "TFBotSpawner: Invalid attribute 'health regen'\n" ); + } + else + { + CAttributeList *pAttrList = me->GetAttributeList(); + if ( pAttrList ) + { + pAttrList->SetRuntimeAttributeValue( pAttrDef_HealthRegen, tf_mvm_bot_flag_carrier_health_regen.GetFloat() ); + } + } + + // update the objective resource so clients have the information + if ( TFObjectiveResource() ) + { + TFObjectiveResource()->SetFlagCarrierUpgradeLevel( 2 ); + TFObjectiveResource()->SetBaseMvMBombUpgradeTime( gpGlobals->curtime ); + TFObjectiveResource()->SetNextMvMBombUpgradeTime( gpGlobals->curtime + m_upgradeTimer.GetRemainingTime() ); + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_BOMB_CARRIER_UPGRADE2, TF_TEAM_PVE_DEFENDERS ); + DispatchParticleEffect( "mvm_levelup2", PATTACH_POINT_FOLLOW, me, "head" ); + } + return true; + } + + //--------------------------------------- + case 3: + // add critz + me->m_Shared.AddCond( TF_COND_CRITBOOSTED ); + + // update the objective resource so clients have the information + if ( TFObjectiveResource() ) + { + TFObjectiveResource()->SetFlagCarrierUpgradeLevel( 3 ); + TFObjectiveResource()->SetBaseMvMBombUpgradeTime( -1 ); + TFObjectiveResource()->SetNextMvMBombUpgradeTime( -1 ); + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_BOMB_CARRIER_UPGRADE3, TF_TEAM_PVE_DEFENDERS ); + DispatchParticleEffect( "mvm_levelup3", PATTACH_POINT_FOLLOW, me, "head" ); + } + return true; + } + } + } + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDeliverFlag::Update( CTFBot *me, float interval ) +{ + CCaptureFlag *flag = me->GetFlagToFetch(); + + if ( !flag ) + { + return Done( "No flag" ); + } + + CTFPlayer *carrier = ToTFPlayer( flag->GetOwnerEntity() ); + if ( !carrier || !me->IsSelf( carrier ) ) + { + return Done( "I'm no longer carrying the flag" ); + } + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + // let the bomb carrier use it's buff banners/etc + Action< CTFBot > *result = me->OpportunisticallyUseWeaponAbilities(); + if ( result ) + { + return SuspendFor( result, "Opportunistically using buff item" ); + } + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + // prepare to fight + me->EquipBestWeaponForThreat( threat ); + } + + // deliver the flag + if ( m_repathTimer.IsElapsed() ) + { + CCaptureZone *zone = me->GetFlagCaptureZone(); + + if ( !zone ) + { + return Done( "No flag capture zone exists!" ); + } + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, zone->WorldSpaceCenter(), cost ); + + float flOldTravelDistance = m_flTotalTravelDistance; + + m_flTotalTravelDistance = NavAreaTravelDistance( me->GetLastKnownArea(), TheNavMesh->GetNavArea( zone->WorldSpaceCenter() ), cost ); + + if ( flOldTravelDistance != -1.0f && m_flTotalTravelDistance - flOldTravelDistance > 2000.0f ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Bomb_Reset" ); + + // Look for players that helped with the reset and send an event + CUtlVector<CTFPlayer *> playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS ); + FOR_EACH_VEC( playerVector, i ) + { + CTFPlayer *pPlayer = playerVector[i]; + if ( !pPlayer ) + continue; + + if ( me->m_AchievementData.IsPusherInHistory( pPlayer, 3.f ) ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_bomb_reset_by_player" ); + if ( event ) + { + event->SetInt( "player", pPlayer->entindex() ); + gameeventmanager->FireEvent( event ); + } + + CTF_GameStats.Event_PlayerAwardBonusPoints( pPlayer, me, 100 ); + } + } + } + + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + } + + m_path.Update( me ); + + if ( UpgradeOverTime( me ) ) + { + return SuspendFor( new CTFBotTaunt, "Taunting for our new upgrade" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotDeliverFlag::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + me->ClearAttribute( CTFBot::SUPPRESS_FIRE ); + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + me->m_Shared.ResetRageBuffs(); + } +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotDeliverFlag::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + if ( tf_mvm_bot_allow_flag_carrier_to_fight.GetBool() ) + { + return ANSWER_UNDEFINED; + } + + return ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +// are we in a hurry? +QueryResultType CTFBotDeliverFlag::ShouldHurry( const INextBot *me ) const +{ + return ANSWER_YES; +} + + +//--------------------------------------------------------------------------------------------- +// is it time to retreat? +QueryResultType CTFBotDeliverFlag::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotDeliverFlag::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result ) +{ + if ( TFGameRules()->IsMannVsMachineMode() && other && FClassnameIs( other, "func_capturezone" ) ) + { + return TrySuspendFor( new CTFBotMvMDeployBomb, RESULT_CRITICAL, "Delivering the bomb!" ); + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CTFBotPushToCapturePoint::CTFBotPushToCapturePoint( Action< CTFBot > *nextAction ) +{ + m_nextAction = nextAction; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPushToCapturePoint::Update( CTFBot *me, float interval ) +{ + // flag collection and delivery is handled by our parent behavior, ScenarioMonitor + + CCaptureZone *zone = me->GetFlagCaptureZone(); + + if ( !zone ) + { + if ( m_nextAction ) + { + return ChangeTo( m_nextAction, "No flag capture zone exists!" ); + } + + return Done( "No flag capture zone exists!" ); + } + + Vector toZone = zone->WorldSpaceCenter() - me->GetAbsOrigin(); + if ( toZone.AsVector2D().IsLengthLessThan( 50.0f ) ) + { + if ( m_nextAction ) + { + return ChangeTo( m_nextAction, "At destination" ); + } + + return Done( "At destination" ); + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + // prepare to fight + me->EquipBestWeaponForThreat( threat ); + } + + if ( m_repathTimer.IsElapsed() ) + { + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, zone->WorldSpaceCenter(), cost ); + + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + } + + m_path.Update( me ); + + return Continue(); +} + +//----------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPushToCapturePoint::OnNavAreaChanged( CTFBot *me, CNavArea *newArea, CNavArea *oldArea ) +{ + // does the area we are entering have a prerequisite? + if ( newArea && newArea->HasPrerequisite( me ) ) + { + const CUtlVector< CHandle< CFuncNavPrerequisite > > &prereqVector = newArea->GetPrerequisiteVector(); + + for( int i=0; i<prereqVector.Count(); ++i ) + { + const CFuncNavPrerequisite *prereq = prereqVector[i]; + if ( prereq && prereq->IsEnabled() && const_cast< CFuncNavPrerequisite * >( prereq )->PassesTriggerFilters( me ) ) + { + // this prerequisite applies to me + if ( prereq->IsTask( CFuncNavPrerequisite::TASK_WAIT ) ) + { + return TrySuspendFor( new CTFBotNavEntWait( prereq ), RESULT_IMPORTANT, "Prerequisite commands me to wait" ); + } + else if ( prereq->IsTask( CFuncNavPrerequisite::TASK_MOVE_TO_ENTITY ) ) + { + return TrySuspendFor( new CTFBotNavEntMoveTo( prereq ), RESULT_IMPORTANT, "Prerequisite commands me to move to an entity" ); + } + } + } + } + + return TryContinue(); +} diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h new file mode 100644 index 0000000..1e8dfdf --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h @@ -0,0 +1,63 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_deliver_flag.h +// Take the flag we are holding to its destination +// Michael Booth, May 2011 + +#ifndef TF_BOT_DELIVER_FLAG_H +#define TF_BOT_DELIVER_FLAG_H + +#include "Path/NextBotPathFollow.h" + + +//----------------------------------------------------------------------------- +class CTFBotDeliverFlag : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; + virtual QueryResultType ShouldHurry( const INextBot *me ) const; + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; + + virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual const char *GetName( void ) const { return "DeliverFlag"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; + float m_flTotalTravelDistance; + + bool UpgradeOverTime( CTFBot *me ); + CountdownTimer m_upgradeTimer; + +#define DONT_UPGRADE -1 + int m_upgradeLevel; + + CountdownTimer m_buffPulseTimer; +}; + + +//----------------------------------------------------------------------------- +class CTFBotPushToCapturePoint : public Action< CTFBot > +{ +public: + CTFBotPushToCapturePoint( Action< CTFBot > *nextAction = NULL ); + virtual ~CTFBotPushToCapturePoint() { } + + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual EventDesiredResult< CTFBot > OnNavAreaChanged( CTFBot *me, CNavArea *newArea, CNavArea *oldArea ); + + virtual const char *GetName( void ) const { return "PushToCapturePoint"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; + + Action< CTFBot > *m_nextAction; +}; + + +#endif // TF_BOT_DELIVER_FLAG_H diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.cpp b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.cpp new file mode 100644 index 0000000..ca48e19 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.cpp @@ -0,0 +1,142 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_escort_flag_carrier.cpp +// Escort the flag carrier to their destination +// Michael Booth, May 2011 + +#include "cbase.h" + +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h" + +extern ConVar tf_bot_flag_escort_range; + +ConVar tf_bot_flag_escort_give_up_range( "tf_bot_flag_escort_give_up_range", "1000", FCVAR_CHEAT ); +ConVar tf_bot_flag_escort_max_count( "tf_bot_flag_escort_max_count", "4", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +// +// Count the number of TFBots currently engaged in the "EscortFlagCarrier" behavior +// +int GetBotEscortCount( int team ) +{ + int count = 0; + + CUtlVector< CTFPlayer * > livePlayerVector; + CollectPlayers( &livePlayerVector, team, COLLECT_ONLY_LIVING_PLAYERS ); + + int i; + for( i=0; i<livePlayerVector.Count(); ++i ) + { + CTFBot *bot = dynamic_cast< CTFBot * >( livePlayerVector[i] ); + if ( bot ) + { + Behavior< CTFBot > *behavior = (Behavior< CTFBot > *)bot->GetIntentionInterface()->FirstContainedResponder(); + if ( behavior ) + { + Action< CTFBot > *action = (Action< CTFBot > *)behavior->FirstContainedResponder(); + + while( action && action->GetActiveChildAction() ) + { + action = action->GetActiveChildAction(); + } + + if ( action && action->IsNamed( "EscortFlagCarrier" ) ) + { + ++count; + } + } + } + } + + return count; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEscortFlagCarrier::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEscortFlagCarrier::Update( CTFBot *me, float interval ) +{ + CCaptureFlag *flag = me->GetFlagToFetch(); + + if ( !flag ) + { + return Done( "No flag" ); + } + + CTFPlayer *carrier = ToTFPlayer( flag->GetOwnerEntity() ); + if ( !carrier ) + { + return Done( "Flag was dropped" ); + } + else if ( me->IsSelf( carrier ) ) + { + return Done( "I picked up the flag!" ); + } + + // stay near the carrier + if ( me->IsRangeGreaterThan( carrier, tf_bot_flag_escort_give_up_range.GetFloat() ) ) + { + if ( me->SelectRandomReachableEnemy() ) + { + // too far away - give up + return ChangeTo( new CTFBotAttackFlagDefenders, "Too far from flag carrier - attack defenders!" ); + } + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + // prepare to fight + me->EquipBestWeaponForThreat( threat ); + } + + CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon(); + if ( myWeapon && myWeapon->IsMeleeWeapon() ) + { + if ( me->IsRangeLessThan( carrier, tf_bot_flag_escort_range.GetFloat() ) && me->IsLineOfSightClear( carrier ) ) + { + ActionResult< CTFBot > result = m_meleeAttackAction.Update( me, interval ); + + if ( result.IsContinue() ) + { + // we have a melee target, and we're still reasonably close to the flag carrier + return Continue(); + } + } + } + + if ( me->IsRangeGreaterThan( carrier, 0.5f * tf_bot_flag_escort_range.GetFloat() ) ) + { + // move near carrier + if ( m_repathTimer.IsElapsed() ) + { + if ( GetBotEscortCount( me->GetTeamNumber() ) > tf_bot_flag_escort_max_count.GetInt() ) + { + if ( me->SelectRandomReachableEnemy() ) + { + return Done( "Too many flag escorts - giving up" ); + } + } + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, carrier, cost ); + + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + } + + m_path.Update( me ); + } + + return Continue(); +} diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h new file mode 100644 index 0000000..f8e5f9d --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h @@ -0,0 +1,31 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_escort_flag_carrier.h +// Escort the flag carrier to their destination +// Michael Booth, May 2011 + +#ifndef TF_BOT_ESCORT_FLAG_CARRIER_H +#define TF_BOT_ESCORT_FLAG_CARRIER_H + + +#include "Path/NextBotPathFollow.h" +#include "bot/behavior/tf_bot_melee_attack.h" + + +//----------------------------------------------------------------------------- +class CTFBotEscortFlagCarrier : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "EscortFlagCarrier"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; + + CTFBotMeleeAttack m_meleeAttackAction; +}; + + +#endif // TF_BOT_ESCORT_FLAG_CARRIER_H diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.cpp b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.cpp new file mode 100644 index 0000000..84e02ce --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.cpp @@ -0,0 +1,128 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_fetch_flag.cpp +// Go get the flag! +// Michael Booth, May 2011 + +#include "cbase.h" + +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_escort_flag_carrier.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_attack_flag_defenders.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h" + + +//--------------------------------------------------------------------------------------------- +CTFBotFetchFlag::CTFBotFetchFlag( bool isTemporary ) +{ + m_isTemporary = isTemporary; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotFetchFlag::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotFetchFlag::Update( CTFBot *me, float interval ) +{ + CCaptureFlag *flag = me->GetFlagToFetch(); + + if ( !flag ) + { + if ( TFGameRules()->IsMannVsMachineMode() ) + { + return SuspendFor( new CTFBotAttackFlagDefenders, "Flag flag exists - Attacking the enemy flag defenders" ); + } + + return Done( "No flag" ); + } + + // uncloak so we can attack + if ( me->m_Shared.IsStealthed() ) + { + me->PressAltFireButton(); + } + + if ( TFGameRules()->IsMannVsMachineMode() && flag->IsHome() ) + { + if ( gpGlobals->curtime - me->GetSpawnTime() < 1.0f && me->GetTeamNumber() != TEAM_SPECTATOR ) + { + // we just spawned - give us the flag + flag->PickUp( me, true ); + } + else + { + if ( m_isTemporary ) + { + return Done( "Flag unreachable" ); + } + + // flag is at home and we're out in the world - can't reach it + return SuspendFor( new CTFBotAttackFlagDefenders, "Flag unreachable at home - Attacking the enemy flag defenders" ); + } + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat ) + { + me->EquipBestWeaponForThreat( threat ); + } + + CTFPlayer *carrier = ToTFPlayer( flag->GetOwnerEntity() ); + if ( carrier ) + { + if ( m_isTemporary ) + { + return Done( "Someone else picked up the flag" ); + } + + // NOTE: if I've picked up the flag, the ScenarioMonitor will handle it + return SuspendFor( new CTFBotAttackFlagDefenders, "Someone has the flag - attacking the enemy defenders" ); + } + + // go pick up the flag + if ( m_repathTimer.IsElapsed() ) + { + CTFBotPathCost cost( me, DEFAULT_ROUTE ); + float maxPathLength = TFGameRules()->IsMannVsMachineMode() ? TFBOT_MVM_MAX_PATH_LENGTH : 0.0f; + if ( m_path.Compute( me, flag->WorldSpaceCenter(), cost, maxPathLength ) == false ) + { + if ( flag->IsDropped() ) + { + // flag is unreachable - attack for awhile and hope someone else can dislodge it + return SuspendFor( new CTFBotAttackFlagDefenders( RandomFloat( 5.0f, 10.0f ) ), "Flag unreachable - Attacking" ); + + // just give it to me + // flag->PickUp( me, true ); + } + } + + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + } + + m_path.Update( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +// are we in a hurry? +QueryResultType CTFBotFetchFlag::ShouldHurry( const INextBot *me ) const +{ + return ANSWER_YES; +} + + +//--------------------------------------------------------------------------------------------- +// is it time to retreat? +QueryResultType CTFBotFetchFlag::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_NO; +} diff --git a/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h new file mode 100644 index 0000000..5ca8818 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h @@ -0,0 +1,35 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_fetch_flag.h +// Go get the flag! +// Michael Booth, May 2011 + +#ifndef TF_BOT_FETCH_FLAG_H +#define TF_BOT_FETCH_FLAG_H + +#include "Path/NextBotPathFollow.h" + + +//----------------------------------------------------------------------------- +class CTFBotFetchFlag : public Action< CTFBot > +{ +public: + #define TEMPORARY_FLAG_FETCH true + CTFBotFetchFlag( bool isTemporary = false ); + virtual ~CTFBotFetchFlag() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual QueryResultType ShouldHurry( const INextBot *me ) const; + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; + + virtual const char *GetName( void ) const { return "FetchFlag"; }; + +private: + bool m_isTemporary; + PathFollower m_path; + CountdownTimer m_repathTimer; +}; + + +#endif // TF_BOT_FETCH_FLAG_H diff --git a/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.cpp b/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.cpp new file mode 100644 index 0000000..fb93882 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.cpp @@ -0,0 +1,240 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_creep_wave.cpp +// Move in a "creep wave" to the next available control point to capture it +// Michael Booth, August 2010 + +#include "cbase.h" + +#ifdef TF_CREEP_MODE + +#include "team.h" +#include "team_control_point_master.h" +#include "bot/tf_bot_manager.h" +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/creep_wave/tf_bot_creep_wave.h" + +ConVar tf_creep_aggro_range( "tf_creep_aggro_range", "250" ); +ConVar tf_creep_give_up_range( "tf_creep_give_up_range", "300" ); + + +CTFPlayer *FindNearestEnemy( CTFBot *me, float maxRange ) +{ + CBasePlayer *closest = NULL; + float closeRangeSq = maxRange * maxRange; + + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast< CBasePlayer * >( UTIL_PlayerByIndex( i ) ); + + if ( !player ) + continue; + + if ( FNullEnt( player->edict() ) ) + continue; + + if ( me->IsFriend( player ) ) + continue; + + if ( !player->IsAlive() ) + continue; + + float rangeSq = me->GetRangeSquaredTo( player ); + if ( rangeSq < closeRangeSq ) + { + if ( me->IsLineOfFireClear( player ) ) + { + closeRangeSq = rangeSq; + closest = player; + } + } + } + + return (CTFPlayer *)closest; +} + + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotCreepWave::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + me->StopLookingAroundForEnemies(); + m_stuckTimer.Invalidate(); + + me->GetPlayerClass()->SetCustomModel( "models/bots/bot_heavy.mdl", USE_CLASS_ANIMATIONS ); + me->UpdateModel(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotCreepWave::Update( CTFBot *me, float interval ) +{ + if ( !me->IsAlive() && me->StateGet() != TF_STATE_DYING ) + { + // remove dead creeps for now + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", me->GetUserID() ) ); + } + + CBaseCombatWeapon *melee = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE ); + if ( melee ) + { + me->Weapon_Switch( melee ); + } + + CUtlVector< CTeamControlPoint * > captureVector; + TFGameRules()->CollectCapturePoints( me, &captureVector ); + + if ( captureVector.Count() == 0 ) + { + return Continue(); + } + + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, captureVector[0]->WorldSpaceCenter(), cost ); + } + + m_path.Update( me ); + + if ( m_stuckTimer.HasStarted() ) + { + // juke and dodge to escape stuck situation + switch( RandomInt( 0, 3 ) ) + { + case 0: me->PressBackwardButton(); break; + case 1: me->PressForwardButton(); break; + case 2: me->PressLeftButton(); break; + case 3: me->PressRightButton(); break; + } + } + + CTFPlayer *enemy = FindNearestEnemy( me, tf_creep_aggro_range.GetFloat() ); + if ( enemy ) + { + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_INCOMING ); + return SuspendFor( new CTFBotCreepAttack( enemy ), "Attacking nearby enemy" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotCreepWave::OnKilled( CTFBot *me, const CTakeDamageInfo &info ) +{ + if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() && me->IsEnemy( info.GetAttacker() ) ) + { + TheTFBots().OnCreepKilled( ToTFPlayer( info.GetAttacker() ) ); + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotCreepWave::OnStuck( CTFBot *me ) +{ + m_stuckTimer.Start(); + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotCreepWave::OnUnStuck( CTFBot *me ) +{ + m_stuckTimer.Invalidate(); + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CTFBotCreepAttack::CTFBotCreepAttack( CTFPlayer *victim ) +{ + m_victim = victim; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotCreepAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotCreepAttack::Update( CTFBot *me, float interval ) +{ + if ( !me->IsAlive() && me->StateGet() != TF_STATE_DYING ) + { + // remove dead creeps for now + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", me->GetUserID() ) ); + } + + if ( m_victim.Get() == NULL || !m_victim->IsAlive() ) + { + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_JEERS ); + return Done( "Killed victim" ); + } + + if ( me->IsRangeGreaterThan( m_victim, tf_creep_give_up_range.GetFloat() ) || + !me->IsLineOfFireClear( m_victim ) ) + { + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_NEGATIVE ); + return Done( "Lost victim" ); + } + + CTFPlayer *newVictim = FindNearestEnemy( me, tf_creep_aggro_range.GetFloat() ); + if ( newVictim ) + { + float newRangeSq = me->GetRangeSquaredTo( newVictim ); + float victimRangeSq = me->GetRangeSquaredTo( m_victim ); + + if ( newRangeSq < victimRangeSq ) + { + // switch to closer target + m_victim = newVictim; + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_BATTLECRY ); + } + } + + CBaseCombatWeapon *melee = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE ); + if ( melee ) + { + me->Weapon_Switch( melee ); + } + + me->GetBodyInterface()->AimHeadTowards( m_victim, IBody::CRITICAL, 0.2f, NULL, "Looking at enemy" ); + + // swing weapon + me->PressFireButton(); + + // beeline towards our victim + const float combatRange = 40.0f; + if ( me->IsRangeGreaterThan( m_victim, combatRange ) ) + { + me->GetLocomotionInterface()->Approach( m_victim->GetAbsOrigin() ); + } + else + { + // juke and dodge to avoid interpenetration + switch( RandomInt( 0, 3 ) ) + { + case 0: me->PressBackwardButton(); break; + case 1: me->PressForwardButton(); break; + case 2: me->PressLeftButton(); break; + case 3: me->PressRightButton(); break; + } + } + + return Continue(); +} + + + + +#endif // TF_CREEP_MODE
\ No newline at end of file diff --git a/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.h b/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.h new file mode 100644 index 0000000..6dedafe --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/creep_wave/tf_bot_creep_wave.h @@ -0,0 +1,57 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_creep_wave.h +// Move in a "creep wave" to the next available control point to capture it +// Michael Booth, August 2010 + +#ifndef TF_BOT_CREEP_WAVE_H +#define TF_BOT_CREEP_WAVE_H + +#ifdef TF_CREEP_MODE + +#include "Path/NextBotPathFollow.h" +#include "Path/NextBotChasePath.h" + + +CTFBot *FindNearestEnemyCreep( CTFBot *me ); + + +//----------------------------------------------------------------------------- +class CTFBotCreepWave : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnKilled( CTFBot *me, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnUnStuck( CTFBot *me ); + + virtual const char *GetName( void ) const { return "CreepWave"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; + IntervalTimer m_stuckTimer; +}; + + + +//----------------------------------------------------------------------------- +class CTFBotCreepAttack : public Action< CTFBot > +{ +public: + CTFBotCreepAttack( CTFPlayer *victim ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "CreepAttack"; }; + +private: + CHandle< CTFPlayer > m_victim; +}; + + +#endif // TF_CREEP_MODE + +#endif // TF_BOT_CREEP_WAVE_H diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.cpp b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.cpp new file mode 100644 index 0000000..3f34071 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.cpp @@ -0,0 +1,151 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_payload_block.cpp +// Prevent the other team from moving the cart +// Michael Booth, April 2010 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "team_control_point_master.h" +#include "team_train_watcher.h" +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/payload/tf_bot_payload_block.h" +#include "bot/behavior/medic/tf_bot_medic_heal.h" +#include "bot/behavior/engineer/tf_bot_engineer_build.h" + + +extern ConVar tf_bot_path_lookahead_range; + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPayloadBlock::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + m_path.Invalidate(); + + m_giveUpTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPayloadBlock::Update( CTFBot *me, float interval ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + // prepare to fight + me->EquipBestWeaponForThreat( threat ); + } + + if ( m_giveUpTimer.IsElapsed() ) + { + return Done( "Been blocking long enough" ); + } + + // move toward the point, periodically repathing to account for changing situation + if ( m_repathTimer.IsElapsed() ) + { + VPROF_BUDGET( "CTFBotPayloadBlock::Update( repath )", "NextBot" ); + + CTeamTrainWatcher *trainWatcher = TFGameRules()->GetPayloadToBlock( me->GetTeamNumber() ); + if ( !trainWatcher ) + { + return Done( "Train Watcher is missing" ); + } + + CBaseEntity *cart = trainWatcher->GetTrainEntity(); + if ( !cart ) + { + return Done( "Cart is missing" ); + } + + CTFBotPathCost cost( me, DEFAULT_ROUTE ); + m_path.Compute( me, cart->WorldSpaceCenter(), cost ); + m_repathTimer.Start( RandomFloat( 0.2f, 0.4f ) ); + } + + // move towards next capture point + m_path.Update( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPayloadBlock::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + VPROF_BUDGET( "CTFBotPayloadBlock::OnResume", "NextBot" ); + + m_repathTimer.Invalidate(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnStuck( CTFBot *me ) +{ + VPROF_BUDGET( "CTFBotPayloadBlock::OnStuck", "NextBot" ); + + m_repathTimer.Invalidate(); + me->GetLocomotionInterface()->ClearStuckStatus(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + VPROF_BUDGET( "CTFBotPayloadBlock::OnMoveToFailure", "NextBot" ); + + m_repathTimer.Invalidate(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnTerritoryContested( CTFBot *me, int territoryID ) +{ + return TryToSustain( RESULT_IMPORTANT ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnTerritoryCaptured( CTFBot *me, int territoryID ) +{ + return TryToSustain( RESULT_IMPORTANT ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadBlock::OnTerritoryLost( CTFBot *me, int territoryID ) +{ + return TryToSustain( RESULT_IMPORTANT ); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotPayloadBlock::ShouldRetreat( const INextBot *bot ) const +{ + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotPayloadBlock::ShouldHurry( const INextBot *bot ) const +{ + // hurry and block the cart - don't retreat, etc + return ANSWER_YES; +} + diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.h b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.h new file mode 100644 index 0000000..9b934f2 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_block.h @@ -0,0 +1,38 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_payload_block.h +// Prevent the other team from moving the cart +// Michael Booth, April 2010 + +#ifndef TF_BOT_PAYLOAD_BLOCK_H +#define TF_BOT_PAYLOAD_BLOCK_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotPayloadBlock : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID ); + + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + + virtual const char *GetName( void ) const { return "PayloadBlock"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; + + CountdownTimer m_giveUpTimer; +}; + +#endif // TF_BOT_PAYLOAD_BLOCK_H diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.cpp b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.cpp new file mode 100644 index 0000000..a09d186 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.cpp @@ -0,0 +1,283 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_payload_guard.cpp +// Guard the payload and keep the attackers from getting near it +// Michael Booth, April 2010 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "team_control_point_master.h" +#include "team_train_watcher.h" +#include "trigger_area_capture.h" +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/payload/tf_bot_payload_guard.h" +#include "bot/behavior/scenario/payload/tf_bot_payload_block.h" +#include "bot/behavior/medic/tf_bot_medic_heal.h" +#include "bot/behavior/engineer/tf_bot_engineer_build.h" +#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h" + + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_payload_guard_range( "tf_bot_payload_guard_range", "1000", FCVAR_CHEAT ); +ConVar tf_bot_debug_payload_guard_vantage_points( "tf_bot_debug_payload_guard_vantage_points", 0, FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPayloadGuard::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + m_path.Invalidate(); + + m_vantagePoint = me->GetAbsOrigin(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPayloadGuard::Update( CTFBot *me, float interval ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + // prepare to fight + me->EquipBestWeaponForThreat( threat ); + } + + CTeamTrainWatcher *trainWatcher = TFGameRules()->GetPayloadToBlock( me->GetTeamNumber() ); + if ( !trainWatcher ) + { + return Continue(); + } + + CBaseEntity *cart = trainWatcher->GetTrainEntity(); + if ( !cart ) + { + return Continue(); + } + + if ( !trainWatcher->IsDisabled() && trainWatcher->GetCapturerCount() > 0 ) + { + // the cart is being pushed ahead - block it + if ( !m_moveToBlockTimer.HasStarted() ) + { + m_moveToBlockTimer.Start( RandomFloat( 0.5f, 3.0f ) ); + } + } + + if ( m_moveToBlockTimer.HasStarted() && m_moveToBlockTimer.IsElapsed() ) + { + m_moveToBlockTimer.Invalidate(); + + if ( trainWatcher->GetCapturerCount() >= 0 ) + { + // the cart is not yet blocked - move to block it! + return SuspendFor( new CTFBotPayloadBlock, "Moving to block the cart's forward motion" ); + } + } + + bool isMovingToVantagePoint = ( me->GetAbsOrigin() - m_vantagePoint ).AsVector2D().IsLengthGreaterThan( 25.0f ); + + if ( isMovingToVantagePoint ) + { + // en route, don't change the point + m_vantagePointTimer.Start( RandomFloat( 3.0f, 15.0f ) ); + } + + if ( !me->IsLineOfFireClear( cart ) ) + { + // cart is no longer visible from this area, find another one + m_vantagePointTimer.Invalidate(); + } + + if ( m_vantagePointTimer.IsElapsed() ) + { + // find a new vantage point + m_vantagePoint = FindVantagePoint( me, cart ); + m_repathTimer.Invalidate(); + isMovingToVantagePoint = true; + } + + if ( isMovingToVantagePoint ) + { + // update our path periodically + if ( m_repathTimer.IsElapsed() ) + { + CTFBotPathCost cost( me, DEFAULT_ROUTE ); + m_path.Compute( me, m_vantagePoint, cost ); + m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + } + + // move towards our vantage point + m_path.Update( me ); + } + else + { + // at vantage point + if ( CTFBotPrepareStickybombTrap::IsPossible( me ) ) + { + return SuspendFor( new CTFBotPrepareStickybombTrap, "Laying sticky bombs!" ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPayloadGuard::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + VPROF_BUDGET( "CTFBotPayloadGuard::OnResume", "NextBot" ); + + m_vantagePointTimer.Invalidate(); + m_repathTimer.Invalidate(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnStuck( CTFBot *me ) +{ + VPROF_BUDGET( "CTFBotPayloadGuard::OnStuck", "NextBot" ); + + m_repathTimer.Invalidate(); + me->GetLocomotionInterface()->ClearStuckStatus(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + VPROF_BUDGET( "CTFBotPayloadGuard::OnMoveToFailure", "NextBot" ); + + m_vantagePointTimer.Invalidate(); + m_repathTimer.Invalidate(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +// Invoked when cart is being pushed +EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnTerritoryContested( CTFBot *me, int territoryID ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnTerritoryCaptured( CTFBot *me, int territoryID ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +// Invoked when cart hits a checkpoint +EventDesiredResult< CTFBot > CTFBotPayloadGuard::OnTerritoryLost( CTFBot *me, int territoryID ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotPayloadGuard::ShouldRetreat( const INextBot *bot ) const +{ + CTFBot *me = ToTFBot( bot->GetEntity() ); + + CTeamTrainWatcher *trainWatcher = TFGameRules()->GetPayloadToBlock( me->GetTeamNumber() ); + if ( trainWatcher && trainWatcher->IsTrainNearCheckpoint() ) + { + // don't retreat if the cart is almost at the next checkpoint + return ANSWER_NO; + } + + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotPayloadGuard::ShouldHurry( const INextBot *bot ) const +{ + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +class CCollectPayloadGuardVantagePoints : public ISearchSurroundingAreasFunctor +{ +public: + CCollectPayloadGuardVantagePoints( CTFBot *me, CBaseEntity *cart ) + { + m_me = me; + m_cart = cart; + } + + virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) + { + CTFNavArea *area = (CTFNavArea *)baseArea; + + // TODO: only use areas that are at/farther along than the payload + + trace_t trace; + NextBotTraceFilterIgnoreActors filter( NULL, COLLISION_GROUP_NONE ); + + const int tryCount = 3; + + for( int i=0; i<tryCount; ++i ) + { + Vector spot = area->GetRandomPoint(); + Vector eyeSpot = Vector( spot.x, spot.y, spot.z + HumanEyeHeight ); + + UTIL_TraceLine( eyeSpot, m_cart->WorldSpaceCenter(), MASK_SOLID_BRUSHONLY, &filter, &trace ); + + if ( !trace.DidHit() || trace.m_pEnt == m_cart ) + { + m_vantagePointVector.AddToTail( spot ); + + if ( tf_bot_debug_payload_guard_vantage_points.GetBool() ) + { + NDebugOverlay::Cross3D( spot, 5.0f, 255, 0, 255, true, 120.0f ); + } + } + } + + return true; + } + + CTFBot *m_me; + CBaseEntity *m_cart; + CUtlVector< Vector > m_vantagePointVector; +}; + + +//--------------------------------------------------------------------------------------------- +// +// Find a tactically advantageous area where we can see the payload +// +Vector CTFBotPayloadGuard::FindVantagePoint( CTFBot *me, CBaseEntity *cart ) +{ + CTFNavArea *cartArea = (CTFNavArea *)TheNavMesh->GetNearestNavArea( cart ); + + CCollectPayloadGuardVantagePoints collect( me, cart ); + SearchSurroundingAreas( cartArea, collect, tf_bot_payload_guard_range.GetFloat() ); + + if ( collect.m_vantagePointVector.Count() == 0 ) + return cart->WorldSpaceCenter(); + + int which = RandomInt( 0, collect.m_vantagePointVector.Count()-1 ); + return collect.m_vantagePointVector[ which ]; +} + diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.h b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.h new file mode 100644 index 0000000..2d1b03d --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_guard.h @@ -0,0 +1,43 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_payload_guard.h +// Guard the payload and keep the attackers from getting near it +// Michael Booth, April 2010 + +#ifndef TF_BOT_PAYLOAD_GUARD_H +#define TF_BOT_PAYLOAD_GUARD_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotPayloadGuard : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID ); + + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + + virtual const char *GetName( void ) const { return "PayloadGuard"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; + + Vector m_vantagePoint; + CountdownTimer m_vantagePointTimer; + Vector FindVantagePoint( CTFBot *me, CBaseEntity *cart ); + + CountdownTimer m_moveToBlockTimer; + +}; + +#endif // TF_BOT_PAYLOAD_GUARD_H diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.cpp b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.cpp new file mode 100644 index 0000000..79fe667 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.cpp @@ -0,0 +1,155 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_payload_push.cpp +// Push the cartTrigger to the goal +// Michael Booth, April 2010 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "team_control_point_master.h" +#include "team_train_watcher.h" +#include "trigger_area_capture.h" +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/payload/tf_bot_payload_push.h" +#include "bot/behavior/medic/tf_bot_medic_heal.h" +#include "bot/behavior/engineer/tf_bot_engineer_build.h" + + +extern ConVar tf_bot_path_lookahead_range; +ConVar tf_bot_cart_push_radius( "tf_bot_cart_push_radius", "60", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPayloadPush::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + m_path.Invalidate(); + + m_hideAngle = 180.0f; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPayloadPush::Update( CTFBot *me, float interval ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + // prepare to fight + me->EquipBestWeaponForThreat( threat ); + } + + if ( TFGameRules()->InSetup() ) + { + // wait until the gates open, then path + m_path.Invalidate(); + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + return Continue(); + } + + CTeamTrainWatcher *trainWatcher = TFGameRules()->GetPayloadToPush( me->GetTeamNumber() ); + if ( !trainWatcher ) + { + return Continue(); + } + + CBaseEntity *cart = trainWatcher->GetTrainEntity(); + if ( !cart ) + { + return Continue(); + } + + + // move toward the point, periodically repathing to account for changing situation + if ( m_repathTimer.IsElapsed() ) + { + VPROF_BUDGET( "CTFBotPayloadPush::Update( repath )", "NextBot" ); + + Vector cartForward; + cart->GetVectors( &cartForward, NULL, NULL ); + + // default push position is behind cart + Vector pushPos = cart->WorldSpaceCenter() - cartForward * tf_bot_cart_push_radius.GetFloat(); + + // try to hide from enemies on other side of cart + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat ) + { + Vector enemyToCart = cart->WorldSpaceCenter() - threat->GetLastKnownPosition(); + enemyToCart.z = 0.0f; + enemyToCart.NormalizeInPlace(); + + pushPos = cart->WorldSpaceCenter() + tf_bot_cart_push_radius.GetFloat() * enemyToCart; + } + + CTFBotPathCost cost( me, DEFAULT_ROUTE ); + m_path.Compute( me, pushPos, cost ); + + m_repathTimer.Start( RandomFloat( 0.2f, 0.4f ) ); + } + + // push the cartTrigger + m_path.Update( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotPayloadPush::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + VPROF_BUDGET( "CTFBotPayloadPush::OnResume", "NextBot" ); + + m_repathTimer.Invalidate(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadPush::OnStuck( CTFBot *me ) +{ + VPROF_BUDGET( "CTFBotPayloadPush::OnStuck", "NextBot" ); + + m_repathTimer.Invalidate(); + me->GetLocomotionInterface()->ClearStuckStatus(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadPush::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotPayloadPush::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + VPROF_BUDGET( "CTFBotPayloadPush::OnMoveToFailure", "NextBot" ); + + m_repathTimer.Invalidate(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotPayloadPush::ShouldRetreat( const INextBot *bot ) const +{ + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotPayloadPush::ShouldHurry( const INextBot *bot ) const +{ + return ANSWER_UNDEFINED; +} + diff --git a/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.h b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.h new file mode 100644 index 0000000..4499c25 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/payload/tf_bot_payload_push.h @@ -0,0 +1,33 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_payload_push.h +// Push the cart to the goal +// Michael Booth, April 2010 + +#ifndef TF_BOT_PAYLOAD_PUSH_H +#define TF_BOT_PAYLOAD_PUSH_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotPayloadPush : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + + virtual const char *GetName( void ) const { return "PayloadPush"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; + float m_hideAngle; +}; + +#endif // TF_BOT_PAYLOAD_PUSH_H diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.cpp b/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.cpp new file mode 100644 index 0000000..1d78ff3 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.cpp @@ -0,0 +1,217 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_raid_companion.cpp +// Teammate bots for Raid mode +// Michael Booth, October 2009 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "team.h" +#include "bot/tf_bot.h" +#include "team_control_point_master.h" +#include "bot/behavior/medic/tf_bot_medic_heal.h" +#include "bot/behavior/scenario/raid/tf_bot_companion.h" +#include "bot/behavior/tf_bot_attack.h" +#include "bot/behavior/tf_bot_move_to_vantage_point.h" +#include "bot/behavior/engineer/tf_bot_engineer_build.h" +#include "bot/behavior/sniper/tf_bot_sniper_lurk.h" + +#include "bot/map_entities/tf_bot_generator.h" // action point + +ConVar tf_raid_companion_follow_range( "tf_raid_companion_follow_range", "150", FCVAR_CHEAT ); +ConVar tf_raid_companion_allow_bot_leader( "tf_raid_companion_allow_bot_leader", "0", FCVAR_CHEAT ); + +extern ConVar tf_bot_path_lookahead_range; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotCompanion::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +CTFPlayer *CTFBotCompanion::GetLeader( void ) +{ + CTeam *raidingTeam = GetGlobalTeam( TF_TEAM_BLUE ); + CTFPlayer *leader = NULL; + float leaderSpeed = FLT_MAX; + + for( int i=0; i<raidingTeam->GetNumPlayers(); ++i ) + { + CTFPlayer *player = (CTFPlayer *)raidingTeam->GetPlayer(i); + + if ( player->IsBot() && !tf_raid_companion_allow_bot_leader.GetBool() ) + continue; + +/* + if ( player->IsPlayerClass( TF_CLASS_ENGINEER ) || + player->IsPlayerClass( TF_CLASS_SNIPER ) || + player->IsPlayerClass( TF_CLASS_MEDIC ) ) + continue; +*/ + + if ( player->IsAlive() ) + { + float speed = player->GetPlayerClass()->GetMaxSpeed(); + + if ( speed < leaderSpeed ) + { + leader = player; + leaderSpeed = speed; + } + } + } + + return leader; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotCompanion::Update( CTFBot *me, float interval ) +{ + if ( me->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + const CKnownEntity *patient = me->GetVisionInterface()->GetClosestKnown( me->GetTeamNumber() ); + if ( patient ) + { + return SuspendFor( new CTFBotMedicHeal ); + } + } + + CTFPlayer *leader = GetLeader(); + if ( !leader ) + return Continue(); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + + if ( me->IsSelf( leader ) ) + { + const float engageRange = 500.0f; + if ( threat && threat->IsVisibleRecently() && me->IsRangeLessThan( threat->GetEntity(), engageRange ) ) + { + // stop pushing ahead and kill nearby threats + return SuspendFor( new CTFBotAttack, "Attacking nearby threats" ); + } + + // head toward next capture point + CTeamControlPoint *point = me->GetMyControlPoint(); + if ( point ) + { + m_path.Update( me, point, cost ); + } + } + else + { + if ( ( !threat || threat->GetTimeSinceLastSeen() > 3.0f ) && leader->GetTimeSinceLastInjury() < 1.0f ) + { + // we don't see anything, but the leader is under attack - find a better vantage point + const float nearRange = 1000.0f; + return SuspendFor( new CTFBotMoveToVantagePoint( nearRange ), "Moving to where I can see the enemy" ); + } + + if ( leader && me->IsDistanceBetweenGreaterThan( leader, tf_raid_companion_follow_range.GetFloat() ) ) + { + m_path.Update( me, leader, cost ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotCompanion::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_path.Invalidate(); + return Continue(); +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotGuardian::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotGuardian::Update( CTFBot *me, float interval ) +{ + if ( me->GetActionPoint() ) + { + const float atHomeRange = 35.0f; // 25.0f; + const Vector &home = me->GetActionPoint()->GetAbsOrigin(); + + if ( me->IsRangeGreaterThan( home, atHomeRange ) ) + { + if ( m_repathTimer.IsElapsed() && !m_path.IsValid() ) + { + m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, home, cost ); + } + + // move home + m_path.Update( me ); + + return Continue(); + } + } + + // at home + m_path.Invalidate(); + me->SetHomeArea( me->GetLastKnownArea() ); + + if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + return SuspendFor( new CTFBotEngineerBuild ); + } + + if ( me->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + return SuspendFor( new CTFBotSniperLurk ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotGuardian::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_path.Invalidate(); + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGuardian::OnStuck( CTFBot *me ) +{ + m_path.Invalidate(); + return TryContinue( RESULT_IMPORTANT ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGuardian::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + m_path.Invalidate(); + return TryContinue( RESULT_IMPORTANT ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGuardian::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + m_path.Invalidate(); + return TryContinue( RESULT_IMPORTANT ); +} + +#endif // TF_RAID_MODE diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.h b/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.h new file mode 100644 index 0000000..e379b0b --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_companion.h @@ -0,0 +1,57 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_companion.h +// Teammate bots for Raid mode +// Michael Booth, October 2009 + +#ifndef TF_BOT_COMPANION_H +#define TF_BOT_COMPANION_H + +#ifdef TF_RAID_MODE + +#include "Path/NextBotPathFollow.h" +#include "Path/NextBotChasePath.h" + +// +// Friendly teammate bots +// +class CTFBotCompanion : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual const char *GetName( void ) const { return "Companion"; }; + +private: + ChasePath m_path; + CTFPlayer *GetLeader( void ); +}; + + +// +// Friendly defenders of the base +// +class CTFBotGuardian : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual const char *GetName( void ) const { return "Guardian"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; +}; + +#endif // TF_RAID_MODE + +#endif // TF_BOT_COMPANION_H diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.cpp b/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.cpp new file mode 100644 index 0000000..32759d9 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.cpp @@ -0,0 +1,261 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_guard_area.cpp +// Defend an area against intruders +// Michael Booth, October 2009 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "team_control_point_master.h" +#include "econ_entity_creation.h" +#include "bot/behavior/tf_bot_retreat_to_cover.h" +#include "bot/behavior/sniper/tf_bot_sniper_attack.h" +#include "bot/behavior/engineer/tf_bot_engineer_build.h" +#include "bot/behavior/medic/tf_bot_medic_heal.h" +#include "bot/behavior/scenario/raid/tf_bot_wander.h" +#include "bot/behavior/scenario/raid/tf_bot_guard_area.h" +#include "bot/behavior/tf_bot_attack.h" +#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h" + +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; +ConVar tf_bot_guard_aggro_range( "tf_bot_guard_aggro_range", "750", FCVAR_CHEAT ); +//ConVar tf_bot_guard_give_up_range( "tf_bot_guard_give_up_range", "1250", FCVAR_CHEAT ); + +ConVar tf_raid_special_vocalize_min_interval( "tf_raid_special_vocalize_min_interval", "10", FCVAR_CHEAT ); +ConVar tf_raid_special_vocalize_max_interval( "tf_raid_special_vocalize_max_interval", "15", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotGuardArea::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_chasePath.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + +/* + // give this guy a hat! + randomitemcriteria_t criteria; + criteria.iItemLevel = AE_USE_SCRIPT_VALUE; + criteria.iItemQuality = AE_USE_SCRIPT_VALUE; + criteria.vecAbsOrigin = me->GetAbsOrigin(); + criteria.vecAbsAngles = vec3_angle; + + switch( me->GetPlayerClass()->GetClassIndex() ) + { + case TF_CLASS_SCOUT: criteria.pszItemName = "Scout Hat 1"; break; + case TF_CLASS_SNIPER: criteria.pszItemName = "Sniper Hat 1"; break; + case TF_CLASS_SOLDIER: criteria.pszItemName = "Soldier Pot Hat"; break; + case TF_CLASS_DEMOMAN: criteria.pszItemName = "Demo Top Hat"; break; + case TF_CLASS_MEDIC: criteria.pszItemName = "Medic Hat 1"; break; + case TF_CLASS_HEAVYWEAPONS: criteria.pszItemName = "Heavy Ushanka Hat"; break; + case TF_CLASS_PYRO: criteria.pszItemName = "Pyro Chicken Hat"; break; + case TF_CLASS_SPY: criteria.pszItemName = "Spy Derby Hat"; break; + case TF_CLASS_ENGINEER: criteria.pszItemName = "Engineer Hat 1"; break; + default: criteria.pszItemName = ""; break; + } + + CBaseEntity *hat = ItemGeneration()->GenerateRandomItem( &criteria ); + if ( hat ) + { + // Fake global id + static int s_nFakeID = 1; + static_cast< CEconEntity * >( hat )->GetAttributeContainer()->GetItem()->SetItemID( s_nFakeID++ ); + + DispatchSpawn( hat ); + static_cast< CEconEntity * >( hat )->GetAttributeContainer()->GetItem()->GenerateAttributes(); + static_cast< CEconEntity * >( hat )->GiveTo( me ); + } + else + { + Msg( "Failed to create hat\n" ); + } +*/ + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +class CFindVantagePoint : public ISearchSurroundingAreasFunctor +{ +public: + CFindVantagePoint( void ) + { + m_vantageArea = NULL; + } + + virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) + { + if ( travelDistanceSoFar > 2000.0f ) + return false; + + CTFNavArea *area = (CTFNavArea *)baseArea; + + CTeam *raidingTeam = GetGlobalTeam( TF_TEAM_BLUE ); + for( int i=0; i<raidingTeam->GetNumPlayers(); ++i ) + { + CTFPlayer *player = (CTFPlayer *)raidingTeam->GetPlayer(i); + + if ( !player->IsAlive() || !player->GetLastKnownArea() ) + continue; + + CTFNavArea *playerArea = (CTFNavArea *)player->GetLastKnownArea(); + if ( playerArea->IsCompletelyVisible( area ) ) + { + // nearby area from which we can see the enemy team + m_vantageArea = area; + return false; + } + } + + return true; + } + + CTFNavArea *m_vantageArea; +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotGuardArea::Update( CTFBot *me, float interval ) +{ + // emit vocalizations to warn players we're in the area + if ( m_vocalizeTimer.IsElapsed() ) + { + m_vocalizeTimer.Start( RandomFloat( tf_raid_special_vocalize_min_interval.GetFloat(), tf_raid_special_vocalize_max_interval.GetFloat() ) ); + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_JEERS ); + } + + if ( me->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + return SuspendFor( new CTFBotMedicHeal ); + } + + if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + return SuspendFor( new CTFBotEngineerBuild ); + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + m_pathToVantageArea.Invalidate(); + + CTFNavArea *myArea = (CTFNavArea *)me->GetLastKnownArea(); + CTFNavArea *threatArea = (CTFNavArea *)threat->GetLastKnownArea(); + if ( myArea && threatArea ) + { + if ( threatArea->GetIncursionDistance( TF_TEAM_BLUE ) < myArea->GetIncursionDistance( TF_TEAM_BLUE ) ) + { + if ( me->IsRangeGreaterThan( threat->GetLastKnownPosition(), tf_bot_guard_aggro_range.GetFloat() ) ) + { + // threat is far off and hasn't reached us yet - hide until they are closer + return SuspendFor( new CTFBotRetreatToCover, "Hiding until threat gets closer" ); + } + } + } + + // attack! + return SuspendFor( new CTFBotAttack, "Attacking nearby threat" ); + } + else + { + // no enemy is visible + Vector moveTo = me->GetAbsOrigin(); + + // if point is being captured, move to it + CTeamControlPoint *point = me->GetMyControlPoint(); + if ( point && point->LastContestedAt() > 0.0f && ( gpGlobals->curtime - point->LastContestedAt() ) < 5.0f ) + { + // the point is, or was very recently, contested - defend it! + moveTo = point->GetAbsOrigin(); + } + else if ( me->GetHomeArea() ) + { + // no enemy is visible - return to our home position + moveTo = me->GetHomeArea()->GetCenter(); + } + + if ( !m_pathToPoint.IsValid() || m_repathTimer.IsElapsed() ) + { + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_pathToPoint.Compute( me, moveTo, cost ); + m_repathTimer.Start( RandomFloat( 2.0f, 3.0f ) ); + } + + if ( ( me->GetAbsOrigin() - moveTo ).IsLengthGreaterThan( 25.0f ) ) + { + m_pathToPoint.Update( me ); + } + + if ( me->GetHomeArea() == me->GetLastKnownArea() ) + { + // at home + if ( CTFBotPrepareStickybombTrap::IsPossible( me ) ) + { + return SuspendFor( new CTFBotPrepareStickybombTrap, "Laying sticky bombs!" ); + } + } + +/* + // no enemy is visible - move to where we can see them + if ( !m_pathToVantageArea.IsValid() ) + { + CTFNavArea *vantageArea = me->FindVantagePoint(); + if ( vantageArea ) + { + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_pathToVantageArea.Compute( me, vantageArea->GetCenter(), cost ); + } + } + + m_pathToVantageArea.Update( me ); +*/ + } + + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGuardArea::OnStuck( CTFBot *me ) +{ + m_chasePath.Invalidate(); + m_pathToPoint.Invalidate(); + m_pathToVantageArea.Invalidate(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGuardArea::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGuardArea::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotGuardArea::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGuardArea::OnCommandApproach( CTFBot *me, const Vector &pos, float range ) +{ + return TryContinue(); +} + +#endif // TF_RAID_MODE diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.h b/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.h new file mode 100644 index 0000000..891dda7 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_guard_area.h @@ -0,0 +1,39 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_guard_area.h +// Defend an area against intruders +// Michael Booth, October 2009 + +#ifdef TF_RAID_MODE + +#ifndef TF_BOT_GUARD_AREA_H +#define TF_BOT_GUARD_AREA_H + +#include "Path/NextBotChasePath.h" + +class CTFBotGuardArea : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + + virtual EventDesiredResult< CTFBot > OnCommandApproach( CTFBot *me, const Vector &pos, float range ); + + virtual const char *GetName( void ) const { return "GuardArea"; }; + +private: + ChasePath m_chasePath; + PathFollower m_pathToPoint; + PathFollower m_pathToVantageArea; + CountdownTimer m_vocalizeTimer; + CountdownTimer m_repathTimer; +}; + +#endif // TF_RAID_MODE + +#endif // TF_BOT_GUARD_AREA_H diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.cpp b/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.cpp new file mode 100644 index 0000000..f80117d --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.cpp @@ -0,0 +1,164 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_mob_rush.cpp +// A member of a rushing mob of melee attackers +// Michael Booth, October 2009 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "team.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_taunt.h" +#include "bot/behavior/scenario/raid/tf_bot_mob_rush.h" + + +ConVar tf_bot_taunt_range( "tf_bot_taunt_range", "100", FCVAR_CHEAT ); +ConVar tf_raid_mob_rush_vocalize_min_interval( "tf_raid_mob_rush_vocalize_min_interval", "5", FCVAR_CHEAT ); +ConVar tf_raid_mob_rush_vocalize_max_interval( "tf_raid_mob_rush_vocalize_max_interval", "8", FCVAR_CHEAT ); +ConVar tf_raid_mob_avoid_range( "tf_raid_mob_avoid_range", "100", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CTFBotMobRush::CTFBotMobRush( CTFPlayer *victim, float reactionTime ) +{ + m_victim = victim; + + // this isn't strictly correct - we shouldn't start the timer until OnStart + m_reactionTimer.Start( reactionTime ); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMobRush::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_vocalizeTimer.Start( RandomFloat( tf_raid_mob_rush_vocalize_min_interval.GetFloat(), tf_raid_mob_rush_vocalize_max_interval.GetFloat() ) ); + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMobRush::Update( CTFBot *me, float interval ) +{ + // mobs use only their melee weapons + CBaseCombatWeapon *meleeWeapon = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE ); + if ( meleeWeapon ) + { + me->Weapon_Switch( meleeWeapon ); + } + + + if ( m_victim == NULL ) + { + return Done( "No victim" ); + } + + me->GetBodyInterface()->AimHeadTowards( m_victim, IBody::CRITICAL, 1.0f, NULL, "Looking at our melee target" ); + + if ( m_reactionTimer.HasStarted() ) + { + if ( m_reactionTimer.IsElapsed() ) + { + // snap out of it! + me->DoAnimationEvent( PLAYERANIMEVENT_VOICE_COMMAND_GESTURE, ACT_MP_GESTURE_VC_FINGERPOINT_PRIMARY ); + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_BATTLECRY ); + m_reactionTimer.Invalidate(); + } + else + { + // wait for reaction time to elapse + return Continue(); + } + } + + if ( me->IsPlayingGesture( ACT_MP_GESTURE_VC_FINGERPOINT_PRIMARY ) ) + { + // wait for "wake up" anim to finish + return Continue(); + } + + // just keep swinging + me->PressFireButton(); + + // chase them down + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Update( me, m_victim, cost ); + + // avoid friends + CTeam *team = GetGlobalTeam( TF_TEAM_RED ); + for( int t=0; t<team->GetNumPlayers(); ++t ) + { + CTFPlayer *teamMember = (CTFPlayer *)team->GetPlayer(t); + + if ( !teamMember->IsAlive() ) + continue; + + Vector toBuddy = teamMember->GetAbsOrigin() - me->GetAbsOrigin(); + if ( toBuddy.IsLengthLessThan( tf_raid_mob_avoid_range.GetFloat() ) ) + { + float range = toBuddy.NormalizeInPlace(); + + me->GetLocomotionInterface()->Approach( me->GetAbsOrigin() - 100.0f * toBuddy, 1.0f - ( range / tf_raid_mob_avoid_range.GetFloat() ) ); + } + } + + + if ( !m_victim->IsAlive() && me->IsRangeLessThan( m_victim, tf_bot_taunt_range.GetFloat() ) ) + { + // we got 'em! + return ChangeTo( new CTFBotTaunt, "Taunt their corpse" ); + } + + if ( m_vocalizeTimer.IsElapsed() ) + { + m_vocalizeTimer.Start( RandomFloat( tf_raid_mob_rush_vocalize_min_interval.GetFloat(), tf_raid_mob_rush_vocalize_max_interval.GetFloat() ) ); + + if ( me->IsPlayerClass( TF_CLASS_SCOUT ) ) + me->EmitSound( "Scout.MobJabber" ); + else if ( me->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ) + me->EmitSound( "Heavy.MobJabber" ); + else + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_BATTLECRY ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMobRush::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result ) +{ + return TryToSustain( RESULT_CRITICAL, "Ignoring contact" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMobRush::OnInjured( CTFBot *me, const CTakeDamageInfo &info ) +{ + return TryToSustain( RESULT_CRITICAL, "Ignoring injury" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMobRush::OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) +{ + return TryToSustain( RESULT_CRITICAL, "Ignoring friend death" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMobRush::OnStuck( CTFBot *me ) +{ + m_path.Invalidate(); + return TryToSustain( RESULT_CRITICAL ); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotMobRush::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_NO; +} + +#endif // TF_RAID_MODE diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.h b/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.h new file mode 100644 index 0000000..0331acc --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_mob_rush.h @@ -0,0 +1,42 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_mob_rush.h +// A member of a rushing mob of melee attackers +// Michael Booth, October 2009 + +#ifndef TF_BOT_MOB_RUSH_H +#define TF_BOT_MOB_RUSH_H + +#ifdef TF_RAID_MODE + +#include "Path/NextBotChasePath.h" + + +//----------------------------------------------------------------------------- +class CTFBotMobRush : public Action< CTFBot > +{ +public: + CTFBotMobRush( CTFPlayer *victim, float reactionTime = 0.0f ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL ); + virtual EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CTFBot > OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + + QueryResultType ShouldRetreat( const INextBot *me ) const; + + virtual const char *GetName( void ) const { return "MobRush"; }; + +private: + CHandle< CTFPlayer > m_victim; + CountdownTimer m_reactionTimer; + CountdownTimer m_tauntTimer; + CountdownTimer m_vocalizeTimer; + ChasePath m_path; +}; + +#endif // TF_RAID_MODE + +#endif // TF_BOT_MOB_RUSH_H diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.cpp b/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.cpp new file mode 100644 index 0000000..95d4362 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.cpp @@ -0,0 +1,150 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_squad_attack.cpp +// Move and attack as a small, cohesive, group +// Michael Booth, October 2009 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "team.h" +#include "raid/tf_raid_logic.h" +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/raid/tf_bot_wander.h" +#include "bot/behavior/scenario/raid/tf_bot_squad_attack.h" +#include "bot/behavior/medic/tf_bot_medic_heal.h" +#include "bot/behavior/tf_bot_move_to_vantage_point.h" + + +ConVar tf_squad_radius( "tf_squad_radius", "200", FCVAR_CHEAT ); +ConVar tf_squad_debug( "tf_squad_debug", "0", FCVAR_CHEAT ); +ConVar tf_raid_squad_vocalize_min_interval( "tf_raid_squad_vocalize_min_interval", "5", FCVAR_CHEAT ); +ConVar tf_raid_squad_vocalize_max_interval( "tf_raid_squad_vocalize_max_interval", "8", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSquadAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_vocalizeTimer.Start( RandomFloat( tf_raid_squad_vocalize_min_interval.GetFloat(), tf_raid_squad_vocalize_max_interval.GetFloat() ) ); + m_victim = NULL; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +// the leader is the slowest member of the squad +CTFBot *CTFBotSquadAttack::GetSquadLeader( CTFBot *me ) const +{ + CTFBot *leader = NULL; + float leaderSpeed = FLT_MAX; + + CTFBotSquad *squad = me->GetSquad(); + CTFBotSquad::Iterator it; + for( it = squad->GetFirstMember(); it != squad->InvalidIterator(); it = squad->GetNextMember( it ) ) + { + CTFBot *bot = it(); + + float speed = bot->GetPlayerClass()->GetMaxSpeed(); + + if ( speed < leaderSpeed ) + { + leader = bot; + leaderSpeed = speed; + } + } + + return leader; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSquadAttack::Update( CTFBot *me, float interval ) +{ + if ( !me->IsInASquad() ) + return Done( "Not in a squad" ); + + if ( me->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + return SuspendFor( new CTFBotMedicHeal ); + } + + CTFBot *leader = GetSquadLeader( me ); + CTFBotPathCost cost( me, FASTEST_ROUTE ); + + if ( m_victim == NULL || m_victimConsiderTimer.IsElapsed() ) + { + m_victimConsiderTimer.Start( 3.0f ); + + m_victim = TFGameRules()->GetRaidLogic()->SelectRaiderToAttack(); + } + + if ( m_victim ) + { + const float engageRange = 500.0f; + if ( me->IsPlayerClass( TF_CLASS_PYRO ) || + me->IsRangeGreaterThan( m_victim->GetAbsOrigin(), engageRange ) || + !me->GetVisionInterface()->IsAbleToSee( m_victim, IVision::DISREGARD_FOV ) ) + { + if ( me->IsSelf( leader ) || me->IsRangeLessThan( leader, tf_squad_radius.GetFloat() ) ) + { + // chase down the enemy + m_chasePath.Update( me, m_victim, cost ); + } + } + + if ( !me->IsSelf( leader ) && me->IsRangeGreaterThan( leader, 1.25f * tf_squad_radius.GetFloat() ) ) + { + // too far from leader - return to him + m_chasePath.Update( me, leader, cost ); + } + + if ( tf_squad_debug.GetBool() && me->IsSelf( leader ) ) + { + NDebugOverlay::Circle( me->GetAbsOrigin(), 20.0f, 255, 255, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + + CTFBotSquad *squad = me->GetSquad(); + CTFBotSquad::Iterator it; + for( it = squad->GetFirstMember(); it != squad->InvalidIterator(); it = squad->GetNextMember( it ) ) + { + CTFBot *bot = it(); + + if ( me->IsSelf( bot ) ) + continue; + + NDebugOverlay::Line( me->WorldSpaceCenter(), bot->WorldSpaceCenter(), 0, 255, 0, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + + if ( m_vocalizeTimer.IsElapsed() ) + { + m_vocalizeTimer.Start( RandomFloat( tf_raid_squad_vocalize_min_interval.GetFloat(), tf_raid_squad_vocalize_max_interval.GetFloat() ) ); + + if ( me->IsPlayerClass( TF_CLASS_SCOUT ) ) + me->EmitSound( "Scout.MobJabber" ); + else if ( me->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ) + me->EmitSound( "Heavy.MobJabber" ); + else + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_BATTLECRY ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSquadAttack::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_path.Invalidate(); + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSquadAttack::OnStuck( CTFBot *me ) +{ + m_path.Invalidate(); + return TryContinue(); +} + +#endif // TF_RAID_MODE
\ No newline at end of file diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.h b/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.h new file mode 100644 index 0000000..a685c65 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_squad_attack.h @@ -0,0 +1,47 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_squad_attack.h +// Move and attack as a small, cohesive, group +// Michael Booth, October 2009 + +#ifndef TF_BOT_SQUAD_ATTACK_H +#define TF_BOT_SQUAD_ATTACK_H + +#ifdef TF_RAID_MODE + +#include "Path/NextBotPathFollow.h" +#include "Path/NextBotChasePath.h" + + +//----------------------------------------------------------------------------- +class CTFBotSquadAttack : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + + QueryResultType ShouldRetreat( const INextBot *me ) const; + + virtual const char *GetName( void ) const { return "SquadPatrol"; }; + +private: + CountdownTimer m_vocalizeTimer; + PathFollower m_path; + ChasePath m_chasePath; + CHandle< CTFPlayer > m_victim; + CountdownTimer m_victimConsiderTimer; + + CTFBot *GetSquadLeader( CTFBot *me ) const; +}; + +inline QueryResultType CTFBotSquadAttack::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_NO; +} + +#endif // TF_RAID_MODE + +#endif // TF_BOT_SQUAD_ATTACK_H diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.cpp b/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.cpp new file mode 100644 index 0000000..ffd08f4 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.cpp @@ -0,0 +1,175 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_wander.cpp +// Wanderering/idle enemies for Squad Co-op mode +// Michael Booth, October 2009 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "team.h" +#include "raid/tf_raid_logic.h" +#include "bot/tf_bot.h" +#include "bot/behavior/scenario/raid/tf_bot_wander.h" +#include "bot/behavior/scenario/raid/tf_bot_mob_rush.h" + + +ConVar tf_raid_wanderer_aggro_range( "tf_raid_wanderer_aggro_range", "500", FCVAR_CHEAT, "If wanderers see a threat closer than this, they attack" ); +ConVar tf_raid_wanderer_notice_friend_death_range( "tf_raid_wanderer_notice_friend_death_range", "1000", FCVAR_CHEAT, "If a friend dies within this radius of a wanderer, it wakes up and attacks the attacker" ); +ConVar tf_raid_wanderer_reaction_factor( "tf_raid_wanderer_reaction_factor", "1", FCVAR_CHEAT ); +ConVar tf_raid_wanderer_vocalize_min_interval( "tf_raid_wanderer_vocalize_min_interval", "20", FCVAR_CHEAT ); +ConVar tf_raid_wanderer_vocalize_max_interval( "tf_raid_wanderer_vocalize_max_interval", "30", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CTFBotWander::CTFBotWander( void ) +{ +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotWander::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_vocalizeTimer.Start( RandomFloat( tf_raid_wanderer_vocalize_min_interval.GetFloat(), tf_raid_wanderer_vocalize_max_interval.GetFloat() ) ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotWander::Update( CTFBot *me, float interval ) +{ + // mobs use only their melee weapons + CBaseCombatWeapon *meleeWeapon = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE ); + if ( meleeWeapon ) + { + me->Weapon_Switch( meleeWeapon ); + } + + + CTeam *raidingTeam = GetGlobalTeam( TF_TEAM_BLUE ); + + if ( me->HasAttribute( CTFBot::AGGRESSIVE ) ) + { + // I'm a mob rusher - pick a random raider and attack them! + CTFPlayer *victim = TFGameRules()->GetRaidLogic()->SelectRaiderToAttack(); + if ( victim ) + { + return SuspendFor( new CTFBotMobRush( victim ), "Rushing a raider" ); + } + } + else if ( m_visionTimer.IsElapsed() ) + { + // I'm a wanderer - look for very nearby threats + m_visionTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + + // find closest visible raider within aggro range + CTFPlayer *threat = NULL; + float closeThreatRangeSq = tf_raid_wanderer_aggro_range.GetFloat() * tf_raid_wanderer_aggro_range.GetFloat(); + + for( int i=0; i<raidingTeam->GetNumPlayers(); ++i ) + { + CTFPlayer *player = (CTFPlayer *)raidingTeam->GetPlayer(i); + + if ( !player->IsAlive() ) + continue; + + float rangeSq = me->GetRangeSquaredTo( player ); + if ( rangeSq < closeThreatRangeSq ) + { + if ( me->GetVisionInterface()->IsLineOfSightClearToEntity( player ) ) + { + threat = player; + closeThreatRangeSq = rangeSq; + } + } + } + + if ( threat ) + { + return SuspendFor( new CTFBotMobRush( threat ), "Attacking threat!" ); + } + } + + if ( m_vocalizeTimer.IsElapsed() ) + { + m_vocalizeTimer.Start( RandomFloat( tf_raid_wanderer_vocalize_min_interval.GetFloat(), tf_raid_wanderer_vocalize_max_interval.GetFloat() ) ); + + // mouth off + if ( me->IsPlayerClass( TF_CLASS_SCOUT ) ) + me->EmitSound( "Scout.WanderJabber" ); + else + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_JEERS ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotWander::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result ) +{ + if ( other && other->IsPlayer() && me->IsEnemy( other ) ) + { + return TrySuspendFor( new CTFBotMobRush( (CTFPlayer *)other ), RESULT_IMPORTANT, "Attacking threat who touched me!" ); + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotWander::OnInjured( CTFBot *me, const CTakeDamageInfo &info ) +{ + if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() && me->IsEnemy( info.GetAttacker() ) ) + { + return TrySuspendFor( new CTFBotMobRush( (CTFPlayer *)info.GetAttacker() ), RESULT_IMPORTANT, "Attacking threat who attacked me!" ); + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotWander::OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) +{ + if ( victim && me->IsFriend( victim ) ) + { + if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() && me->IsEnemy( info.GetAttacker() ) ) + { + if ( me->IsRangeLessThan( victim, tf_raid_wanderer_notice_friend_death_range.GetFloat() ) ) + { + if ( me->GetVisionInterface()->IsAbleToSee( victim, IVision::DISREGARD_FOV ) && + me->GetVisionInterface()->IsAbleToSee( info.GetAttacker(), IVision::DISREGARD_FOV ) ) + { + float rangeToAttacker = me->GetRangeTo( info.GetAttacker() ); + float reactionTime; + + if ( rangeToAttacker < tf_raid_wanderer_aggro_range.GetFloat() ) + { + reactionTime = 0.0f; + } + else + { + reactionTime = tf_raid_wanderer_reaction_factor.GetFloat() * ( rangeToAttacker - tf_raid_wanderer_aggro_range.GetFloat() ) / tf_raid_wanderer_aggro_range.GetFloat(); + } + + return TrySuspendFor( new CTFBotMobRush( (CTFPlayer *)info.GetAttacker(), reactionTime ), RESULT_IMPORTANT, "Attacking my friend's attacker!" ); + } + } + } + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotWander::OnCommandAttack( CTFBot *me, CBaseEntity *victim ) +{ + return TryContinue(); +} + + +#endif // TF_RAID_MODE diff --git a/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.h b/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.h new file mode 100644 index 0000000..0364903 --- /dev/null +++ b/game/server/tf/bot/behavior/scenario/raid/tf_bot_wander.h @@ -0,0 +1,51 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_wander.h +// Wanderering/idle enemies for Squad Co-op mode +// Michael Booth, October 2009 + +#ifndef TF_BOT_WANDER_H +#define TF_BOT_WANDER_H + +#ifdef TF_RAID_MODE + +//----------------------------------------------------------------------------- +class CTFBotWander : public Action< CTFBot > +{ +public: + CTFBotWander( void ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL ); + virtual EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CTFBot > OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ); + + virtual EventDesiredResult< CTFBot > OnCommandAttack( CTFBot *me, CBaseEntity *victim ); + + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + + virtual const char *GetName( void ) const { return "Wander"; }; + +private: + CountdownTimer m_visionTimer; + CountdownTimer m_vocalizeTimer; +}; + + +inline QueryResultType CTFBotWander::ShouldHurry( const INextBot *me ) const +{ + return ANSWER_YES; +} + + +inline QueryResultType CTFBotWander::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_NO; +} + + +#endif TF_RAID_MODE + +#endif // TF_BOT_WANDER_H diff --git a/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.cpp b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.cpp new file mode 100644 index 0000000..9cf7945 --- /dev/null +++ b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.cpp @@ -0,0 +1,253 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_sniper_attack.h +// Attack a threat as a Sniper +// Michael Booth, February 2009 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_obj_sentrygun.h" +#include "tf_gamerules.h" +#include "bot/tf_bot.h" +#include "bot/behavior/sniper/tf_bot_sniper_attack.h" +#include "bot/behavior/tf_bot_melee_attack.h" +#include "bot/behavior/tf_bot_retreat_to_cover.h" + +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_sniper_flee_range( "tf_bot_sniper_flee_range", "400", FCVAR_CHEAT, "If threat is closer than this, retreat" ); +ConVar tf_bot_sniper_melee_range( "tf_bot_sniper_melee_range", "200", FCVAR_CHEAT, "If threat is closer than this, attack with melee weapon" ); +ConVar tf_bot_sniper_linger_time( "tf_bot_sniper_linger_time", "5", FCVAR_CHEAT, "How long Sniper will wait around after losing his target before giving up" ); + + +//--------------------------------------------------------------------------------------------- +bool CTFBotSniperAttack::IsPossible( CTFBot *me ) +{ + return me->IsPlayerClass( TF_CLASS_SNIPER ) && me->GetVisionInterface()->GetPrimaryKnownThreat() && me->GetVisionInterface()->GetPrimaryKnownThreat()->IsVisibleRecently(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSniperAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSniperAttack::Update( CTFBot *me, float interval ) +{ + // switch to our sniper rifle + CBaseCombatWeapon *myGun = me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ); + if ( myGun ) + { + me->Weapon_Switch( myGun ); + } + + // shoot at bad guys + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + + if ( threat && !threat->GetEntity()->IsAlive() ) + { + // he's dead + threat = NULL; + } + + if ( threat == NULL || !threat->IsVisibleInFOVNow() ) + { + if ( m_lingerTimer.IsElapsed() ) + { + if ( me->m_Shared.InCond( TF_COND_ZOOMED ) ) + { + return Continue(); + } + + return Done( "No threat for awhile" ); + } + + return Continue(); + } + + me->EquipBestWeaponForThreat( threat ); + + if ( me->IsDistanceBetweenLessThan( threat->GetLastKnownPosition(), tf_bot_sniper_flee_range.GetFloat() ) ) + { + return SuspendFor( new CTFBotRetreatToCover, "Retreating from nearby enemy" ); + } + + if ( me->GetTimeSinceLastInjury() < 1.0f ) + { + return SuspendFor( new CTFBotRetreatToCover, "Retreating due to injury" ); + } + + // we have a target + m_lingerTimer.Start( RandomFloat( 0.75f, 1.25f ) * tf_bot_sniper_linger_time.GetFloat() ); + + if ( !me->m_Shared.InCond( TF_COND_ZOOMED ) ) + { + me->PressAltFireButton(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotSniperAttack::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + if ( me->m_Shared.InCond( TF_COND_ZOOMED ) ) + { + // we're leaving to do something else - unzoom + me->PressAltFireButton(); + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSniperAttack::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + if ( me->m_Shared.InCond( TF_COND_ZOOMED ) ) + { + // we're leaving to do something else - unzoom + me->PressAltFireButton(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSniperAttack::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +// given a subject, return the world space position we should aim at +Vector CTFBotSniperAttack::SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const +{ + VPROF_BUDGET( "CTFBotSniperAttack::SelectTargetPoint", "NextBot" ); + + Vector visibleSpot; + + trace_t result; + NextBotTraceFilterIgnoreActors filter( subject, COLLISION_GROUP_NONE ); + + // head, then chest, then feet for the Sniper + + // headshot seems to be a bit higher that EyePosition() + Vector subjectHeadPos( subject->EyePosition() ); + subjectHeadPos.z += 1.0f; + + UTIL_TraceLine( me->GetBodyInterface()->GetEyePosition(), subjectHeadPos, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result ); + if ( result.DidHit() ) + { + UTIL_TraceLine( me->GetBodyInterface()->GetEyePosition(), subject->WorldSpaceCenter(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result ); + + if ( result.DidHit() ) + { + UTIL_TraceLine( me->GetBodyInterface()->GetEyePosition(), subject->GetAbsOrigin(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result ); + } + } + + // even if they aren't visible, we have no way to communicate that out, so pick a reasonable spot + return result.endpos; +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBotSniperAttack::IsImmediateThreat( const CBaseCombatCharacter *subject, const CKnownEntity *threat ) const +{ + if ( subject->InSameTeam( threat->GetEntity() ) ) + return false; + + if ( !threat->GetEntity()->IsAlive() ) + return false; + + const float hiddenAwhile = 3.0f; + if ( !threat->WasEverVisible() || threat->GetTimeSinceLastSeen() > hiddenAwhile ) + return false; + + CTFPlayer *player = ToTFPlayer( threat->GetEntity() ); + + Vector to = subject->GetAbsOrigin() - threat->GetLastKnownPosition(); + float threatRange = to.NormalizeInPlace(); + + if ( player == NULL ) + { + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( threat->GetEntity() ); + if ( sentry ) + { + // are we in range? + if ( threatRange < SENTRY_MAX_RANGE ) + { + // is it pointing at us? + Vector sentryForward; + AngleVectors( sentry->GetTurretAngles(), &sentryForward ); + + if ( DotProduct( to, sentryForward ) > 0.8f ) + { + return true; + } + } + } + return false; + } + + if ( player->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + // is the sniper pointing at me? + Vector sniperForward; + player->EyeVectors( &sniperForward ); + + if ( DotProduct( to, sniperForward ) > 0.8f ) + { + return true; + } + } + +#ifdef TF_RAID_MODE + if ( !TFGameRules()->IsRaidMode() ) + { + } + else +#endif // TF_RAID_MODE + { + if ( player->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + // always try to kill these guys first + return true; + } + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +// return the more dangerous of the two threats to 'subject', or NULL if we have no opinion +const CKnownEntity *CTFBotSniperAttack::SelectMoreDangerousThreat( const INextBot *me, + const CBaseCombatCharacter *subject, + const CKnownEntity *threat1, + const CKnownEntity *threat2 ) const +{ + if ( threat1 && threat2 ) + { + bool isImmediateThreat1 = IsImmediateThreat( subject, threat1 ); + bool isImmediateThreat2 = IsImmediateThreat( subject, threat2 ); + + if ( isImmediateThreat1 && !isImmediateThreat2 ) + { + return threat1; + } + else if ( !isImmediateThreat1 && isImmediateThreat2 ) + { + return threat2; + } + } + + // both or neither are immediate threats - no preference + return NULL; +} diff --git a/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.h b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.h new file mode 100644 index 0000000..7a65102 --- /dev/null +++ b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_attack.h @@ -0,0 +1,37 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_sniper_attack.h +// Attack a threat as a Sniper +// Michael Booth, February 2009 + +#ifndef TF_BOT_SNIPER_ATTACK_H +#define TF_BOT_SNIPER_ATTACK_H + +#include "Path/NextBotChasePath.h" + +class CTFBotSniperAttack : public Action< CTFBot > +{ +public: + static bool IsPossible( CTFBot *me ); // return true if this Action has what it needs to perform right now + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + virtual ActionResult< CTFBot > OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual Vector SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const; // given a subject, return the world space position we should aim at + + virtual const CKnownEntity * SelectMoreDangerousThreat( const INextBot *me, + const CBaseCombatCharacter *subject, + const CKnownEntity *threat1, + const CKnownEntity *threat2 ) const; // return the more dangerous of the two threats to 'subject', or NULL if we have no opinion + + virtual const char *GetName( void ) const { return "SniperAttack"; }; + +private: + CountdownTimer m_lingerTimer; + + bool IsImmediateThreat( const CBaseCombatCharacter *subject, const CKnownEntity *threat ) const; +}; + +#endif // TF_BOT_SNIPER_ATTACK_H diff --git a/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.cpp b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.cpp new file mode 100644 index 0000000..88b5204 --- /dev/null +++ b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.cpp @@ -0,0 +1,628 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_sniper_lurk.h +// Move into position and wait for victims +// Michael Booth, October 2009 + +#include "cbase.h" +#include "tf_player.h" + +#ifdef TF_RAID_MODE +#include "raid/tf_raid_logic.h" +#endif // TF_RAID_MODE + +#include "bot/tf_bot.h" +#include "bot/behavior/sniper/tf_bot_sniper_lurk.h" +#include "bot/behavior/sniper/tf_bot_sniper_attack.h" +#include "bot/behavior/tf_bot_retreat_to_cover.h" +#include "bot/behavior/tf_bot_melee_attack.h" +#include "bot/map_entities/tf_bot_hint.h" + +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; +extern ConVar tf_bot_sniper_flee_range; +extern ConVar tf_bot_sniper_melee_range; +extern ConVar tf_bot_debug_sniper; + +extern float SkewedRandomValue( void ); + +ConVar tf_bot_sniper_patience_duration( "tf_bot_sniper_patience_duration", "10", FCVAR_CHEAT, "How long a Sniper bot will wait without seeing an enemy before picking a new spot" ); +ConVar tf_bot_sniper_target_linger_duration( "tf_bot_sniper_target_linger_duration", "2", FCVAR_CHEAT, "How long a Sniper bot will keep toward at a target it just lost sight of" ); +ConVar tf_bot_sniper_allow_opportunistic( "tf_bot_sniper_allow_opportunistic", "1", FCVAR_NONE, "If set, Snipers will stop on their way to their preferred lurking spot to snipe at opportunistic targets" ); + +ConVar tf_mvm_bot_sniper_target_by_dps( "tf_mvm_bot_sniper_target_by_dps", "1", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "If set, Snipers in MvM mode target the victim that has the highest DPS" ); + +#ifdef STAGING_ONLY +extern ConVar tf_bot_use_items; +#endif + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSniperLurk::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_boredTimer.Start( RandomFloat( 0.9f, 1.1f ) * tf_bot_sniper_patience_duration.GetFloat() ); + + m_homePosition = me->GetAbsOrigin(); + m_isHomePositionValid = false; + m_isAtHome = false; + m_failCount = 0; + + m_isOpportunistic = tf_bot_sniper_allow_opportunistic.GetBool(); + + CTFBotHint *hint = NULL; + while( ( hint = (CTFBotHint *)( gEntList.FindEntityByClassname( hint, "func_tfbot_hint" ) ) ) != NULL ) + { + if ( hint->IsA( CTFBotHint::HINT_SNIPER_SPOT ) ) + { + m_hintVector.AddToTail( hint ); + + // make sure we don't yet own any of these hints + if ( me->IsSelf( hint->GetOwnerEntity() ) ) + { + hint->SetOwnerEntity( NULL ); + } + } + } + + m_priorHint = NULL; + + if ( TFGameRules()->IsMannVsMachineMode() && me->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + // mann vs machine snipers shouldn't stop until they reach their home + //m_isOpportunistic = false; + + // mann vs machine snipers should ignore the scenario and just snipe + me->SetMission( CTFBot::MISSION_SNIPER, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM ); + } + +#ifdef STAGING_ONLY + if ( tf_bot_use_items.GetInt() && ( RandomInt(0, 100) <= tf_bot_use_items.GetInt() ) ) + { + CBaseCombatWeapon *myGun = me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ); + me->Weapon_Detach( myGun ); + UTIL_Remove( myGun ); + + BotGenerateAndWearItem( me, "The Huntsman" ); + } +#endif // STAGING_ONLY + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSniperLurk::Update( CTFBot *me, float interval ) +{ +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() ) + { + } + else +#endif + { + // continuously search for good sniping spots + me->AccumulateSniperSpots(); + + if ( !m_isHomePositionValid ) + { + // just found our first sniper spot - update our home position + FindNewHome( me ); + } + } + + // aim at bad guys + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + + if ( threat && !threat->GetEntity()->IsAlive() ) + { + // he's dead + threat = NULL; + } + + if ( threat && me->GetIntentionInterface()->ShouldAttack( me, threat ) == ANSWER_NO ) + { + threat = NULL; + } + + if ( threat && threat->IsVisibleInFOVNow() ) + { + m_failCount = 0; + + if ( me->IsDistanceBetweenLessThan( threat->GetLastKnownPosition(), tf_bot_sniper_melee_range.GetFloat() ) ) + { + const float giveUpRange = 1.25f * tf_bot_sniper_melee_range.GetFloat(); + return SuspendFor( new CTFBotMeleeAttack( giveUpRange ), "Melee attacking nearby threat" ); + } + } + + bool isSightingRifle = false; + + if ( threat && + threat->GetTimeSinceLastSeen() < tf_bot_sniper_target_linger_duration.GetFloat() && + me->IsLineOfFireClear( threat->GetEntity() ) ) + { + // we see something... + if ( m_isOpportunistic ) + { + // switch to our sniper rifle + CBaseCombatWeapon *myGun = me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ); + if ( myGun ) + { + me->Weapon_Switch( myGun ); + } + + isSightingRifle = true; + m_boredTimer.Reset(); + + if ( !m_isHomePositionValid ) + { + // make this our opportunistic home for awhile + m_homePosition = me->GetAbsOrigin(); + m_boredTimer.Start( RandomFloat( 0.9f, 1.1f ) * tf_bot_sniper_patience_duration.GetFloat() ); + } + } + else + { + // switch to our SMG and fire while we run + CBaseCombatWeapon *myGun = me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ); + if ( myGun ) + { + me->Weapon_Switch( myGun ); + } + } + } + + const float homeRange = 25.0f; // 100.0f; + m_isAtHome = ( me->GetAbsOrigin() - m_homePosition ).AsVector2D().IsLengthLessThan( homeRange ); + + if ( m_isAtHome ) + { + isSightingRifle = true; + + // once we've reached a good home spot, opportunistically attack from there + m_isOpportunistic = tf_bot_sniper_allow_opportunistic.GetBool(); + + if ( m_boredTimer.IsElapsed() ) + { + ++m_failCount; + + if ( FindNewHome( me ) ) + { + me->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_NEGATIVE ); + m_boredTimer.Start( RandomFloat( 0.9f, 1.1f ) * tf_bot_sniper_patience_duration.GetFloat() ); + } + else + { + // try again soon + m_boredTimer.Start( 1.0f ); + } + } + } + else + { + // not yet at home - can't start to be bored + m_boredTimer.Reset(); + } + + if ( isSightingRifle ) + { + // switch to our sniper rifle + CTFWeaponBase *myGun = (CTFWeaponBase *)me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ); + if ( myGun ) + { + me->Weapon_Switch( myGun ); + + if ( !me->m_Shared.InCond( TF_COND_ZOOMED ) && !myGun->IsWeapon( TF_WEAPON_COMPOUND_BOW ) ) + { + // zoom in and stand still + me->PressAltFireButton(); + } + } + } + else + { + // move to our home position + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_path.Compute( me, m_homePosition, cost ); + } + + m_path.Update( me ); + + if ( me->m_Shared.InCond( TF_COND_ZOOMED ) ) + { + me->PressAltFireButton(); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotSniperLurk::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + if ( me->m_Shared.InCond( TF_COND_ZOOMED ) ) + { + // we're leaving to do something else - unzoom + me->PressAltFireButton(); + } + + if ( m_priorHint != NULL ) + { + // release my hint + m_priorHint->SetOwnerEntity( NULL ); + + if ( tf_bot_debug_sniper.GetBool() ) + { + DevMsg( "%3.2f: %s: Releasing hint.\n", gpGlobals->curtime, me->GetPlayerName() ); + } + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSniperLurk::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + if ( me->m_Shared.InCond( TF_COND_ZOOMED ) ) + { + // we're leaving to do something else - unzoom + me->PressAltFireButton(); + } + + if ( m_priorHint != NULL ) + { + // release my hint + m_priorHint->SetOwnerEntity( NULL ); + + if ( tf_bot_debug_sniper.GetBool() ) + { + DevMsg( "%3.2f: %s: Releasing hint.\n", gpGlobals->curtime, me->GetPlayerName() ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSniperLurk::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_repathTimer.Invalidate(); + m_priorHint = NULL; + + // we probably just fetched some health because the enemy shot us - pick a new place to lurk + FindNewHome( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBotSniperLurk::FindHint( CTFBot *me ) +{ + // if any sniper spot hints exist, pick one of them + CUtlVector< CTFBotHint * > activeHintVector; + for( int i=0; i<m_hintVector.Count(); ++i ) + { + if ( m_hintVector[i] != NULL && m_hintVector[i]->IsFor( me ) ) + { + activeHintVector.AddToTail( m_hintVector[i] ); + } + } + + if ( activeHintVector.Count() == 0 ) + { + return false; + } + + if ( m_priorHint != NULL ) + { + // release my hint + m_priorHint->SetOwnerEntity( NULL ); + + if ( tf_bot_debug_sniper.GetBool() ) + { + DevMsg( "%3.2f: %s: Releasing hint.\n", gpGlobals->curtime, me->GetPlayerName() ); + } + } + + CTFBotHint *hint = NULL; + + if ( m_priorHint != NULL && m_failCount < 2 ) + { + // there used to be targets here - pick nearby hint + float nearRange = 500.0f; + CUtlVector< CTFBotHint * > nearHintVector; + for( int i=0; i<activeHintVector.Count(); ++i ) + { + if ( activeHintVector[i] == m_priorHint ) + continue; + + if ( ( activeHintVector[i]->WorldSpaceCenter() - m_priorHint->WorldSpaceCenter() ).IsLengthGreaterThan( nearRange ) ) + continue; + + if ( activeHintVector[i]->GetOwnerEntity() != NULL ) + continue; + + nearHintVector.AddToTail( activeHintVector[i] ); + } + + if ( nearHintVector.Count() == 0 ) + { + ++m_failCount; + return false; + } + + int whichHint = RandomInt( 0, nearHintVector.Count()-1 ); + hint = nearHintVector[ whichHint ]; + } + else + { + // picking either our first hint, or we haven't seen a victim in a long time - pick a hint that can actually see someone + CUtlVector< CTFPlayer * > victimVector; + CollectPlayers( &victimVector, GetEnemyTeam( me->GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS ); + + CUtlVector< CTFBotHint * > hotHintVector; + CUtlVector< CTFBotHint * > freeHintVector; + + for( int i=0; i<activeHintVector.Count(); ++i ) + { + if ( activeHintVector[i]->GetOwnerEntity() != NULL ) + continue; + + freeHintVector.AddToTail( activeHintVector[i] ); + + for( int p=0; p<victimVector.Count(); ++p ) + { + if ( victimVector[p]->IsLineOfSightClear( activeHintVector[i]->WorldSpaceCenter(), CBaseCombatCharacter::IGNORE_ACTORS ) ) + { + // at least one victim is visible from this hint + hotHintVector.AddToTail( activeHintVector[i] ); + break; + } + } + } + + if ( hotHintVector.Count() == 0 ) + { + // no hints can see any victims - pick at random + if ( freeHintVector.Count() == 0 ) + { + // all hints are owned by another sniper - double up + int whichHint = RandomInt( 0, activeHintVector.Count()-1 ); + hint = activeHintVector[ whichHint ]; + + if ( tf_bot_debug_sniper.GetBool() ) + { + DevMsg( "%3.2f: %s: No un-owned hints available! Doubling up.\n", gpGlobals->curtime, me->GetPlayerName() ); + } + } + else + { + int whichHint = RandomInt( 0, freeHintVector.Count()-1 ); + hint = freeHintVector[ whichHint ]; + } + } + else + { + int whichHint = RandomInt( 0, hotHintVector.Count()-1 ); + hint = hotHintVector[ whichHint ]; + } + } + + if ( hint == NULL ) + { + return false; + } + + Extent hintExtent; + hintExtent.Init( hint ); + + Vector hintSpot; + hintSpot.x = RandomFloat( hintExtent.lo.x, hintExtent.hi.x ); + hintSpot.y = RandomFloat( hintExtent.lo.y, hintExtent.hi.y ); + hintSpot.z = ( hintExtent.lo.z + hintExtent.hi.z ) / 2.0f; + + TheNavMesh->GetSimpleGroundHeight( hintSpot, &hintSpot.z ); + + m_homePosition = hintSpot; + m_isHomePositionValid = true; + m_priorHint = hint; + + // my hint + hint->SetOwnerEntity( me ); + + return true; +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBotSniperLurk::FindNewHome( CTFBot *me ) +{ + if ( !m_findHomeTimer.IsElapsed() ) + { + return false; + } + + m_findHomeTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() ) + { + // stay put for now + return true; + } + else +#endif // TF_RAID_MODE + { + // if any sniper spot hints exist, pick one of them + if ( FindHint( me ) ) + { + return true; + } + + // pick a sniper spot from our ongoing search + const CUtlVector< CTFBot::SniperSpotInfo > *sniperSpotVector = me->GetSniperSpots(); + if ( sniperSpotVector->Count() > 0 ) + { + m_homePosition = sniperSpotVector->Element( RandomInt( 0, sniperSpotVector->Count()-1 ) ).m_vantageSpot; + m_isHomePositionValid = true; + return true; + } + } + + // can't find a real sniper spot - pick another goal that will get us out into the fray + m_isHomePositionValid = false; + + // head toward the point + CTeamControlPoint *point = me->GetMyControlPoint(); + if ( point && !point->IsLocked() ) + { + const CUtlVector< CTFNavArea * > *pointAreaVector = TheTFNavMesh()->GetControlPointAreas( point->GetPointIndex() ); + + if ( pointAreaVector && pointAreaVector->Count() > 0 ) + { + int which = RandomInt( 0, pointAreaVector->Count()-1 ); + + m_homePosition = pointAreaVector->Element( which )->GetRandomPoint(); + + return false; + } + } + + // no available point at the moment - head toward the enemy spawn room and opportunistically snipe + CUtlVector< CTFNavArea * > enemySpawnThresholdVector; + TheTFNavMesh()->CollectSpawnRoomThresholdAreas( &enemySpawnThresholdVector, GetEnemyTeam( me->GetTeamNumber() ) ); + + if ( enemySpawnThresholdVector.Count() > 0 ) + { + m_homePosition = enemySpawnThresholdVector[ RandomInt( 0, enemySpawnThresholdVector.Count()-1 ) ]->GetCenter(); + } + else + { + m_homePosition = me->GetAbsOrigin(); + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSniperLurk::ShouldAttack( const INextBot *bot, const CKnownEntity *them ) const +{ + CTFBot *me = (CTFBot *)bot->GetEntity(); + + CTFNavArea *area = me->GetLastKnownArea(); + + if ( TFGameRules()->IsMannVsMachineMode() && area && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) + { + // don't fire while in the spawn area + return ANSWER_NO; + } + + // take the shot if you've got it + return ANSWER_YES; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSniperLurk::ShouldRetreat( const INextBot *me ) const +{ + if ( TFGameRules()->IsMannVsMachineMode() && me->GetEntity()->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + return ANSWER_NO; + } + + return ANSWER_UNDEFINED; +} + +//--------------------------------------------------------------------------------------------- +// Return the more dangerous of the two threats to 'subject', or NULL if we have no opinion +const CKnownEntity *CTFBotSniperLurk::SelectMoreDangerousThreat( const INextBot *meBot, + const CBaseCombatCharacter *subject, + const CKnownEntity *threat1, + const CKnownEntity *threat2 ) const +{ + if ( TFGameRules()->IsMannVsMachineMode() && tf_mvm_bot_sniper_target_by_dps.GetBool() ) + { + CTFBot *me = ToTFBot( meBot->GetEntity() ); + + // If one threat is visible and the other not, always pick the visible one + if ( !threat1->IsVisibleRecently() ) + { + if ( threat2->IsVisibleRecently() ) + { + return threat2; + } + } + else if ( !threat2->IsVisibleRecently() ) + { + return threat1; + } + + // At this point, threat1 and threat2 are either both visible, or both not + + CTFPlayer *playerThreat1 = ToTFPlayer( threat1->GetEntity() ); + CTFPlayer *playerThreat2 = ToTFPlayer( threat2->GetEntity() ); + + if ( playerThreat1 && playerThreat2 ) + { + float rangeSq1 = me->GetRangeSquaredTo( playerThreat1 ); + float rangeSq2 = me->GetRangeSquaredTo( playerThreat2 ); + + if ( me->HasWeaponRestriction( CTFBot::MELEE_ONLY ) ) + { + // Melee-only bots just use closest threat + if ( rangeSq1 < rangeSq2 ) + { + return threat1; + } + return threat2; + } + + // Very near threats are always immediately dangerous + const float nearbyRangeSq = 500.0f * 500.0f; + if ( rangeSq1 < nearbyRangeSq ) + { + if ( rangeSq2 > nearbyRangeSq ) + { + return threat1; + } + } + else if ( rangeSq2 < nearbyRangeSq ) + { + return threat2; + } + + // At this point, both threats are either both very near or both "far" + + // Choose the threat that has the highest DPS + const int equalTolerance = 50; + + if ( playerThreat1->GetDamagePerSecond() > playerThreat2->GetDamagePerSecond() + equalTolerance ) + { + return threat1; + } + else if ( playerThreat2->GetDamagePerSecond() > playerThreat1->GetDamagePerSecond() + equalTolerance ) + { + return threat2; + } + else + { + // approximately equal DPS, choose closest + if ( rangeSq1 < rangeSq2 ) + { + return threat1; + } + return threat2; + } + } + } + + // Use normal threat selection + return NULL; +} diff --git a/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.h b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.h new file mode 100644 index 0000000..fc5b914 --- /dev/null +++ b/game/server/tf/bot/behavior/sniper/tf_bot_sniper_lurk.h @@ -0,0 +1,51 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_sniper_lurk.h +// Move into position and wait for victims +// Michael Booth, October 2009 + +#ifndef TF_BOT_SNIPER_LURK_H +#define TF_BOT_SNIPER_LURK_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotHint; + +class CTFBotSniperLurk : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + virtual ActionResult< CTFBot > OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + // Snipers choose their targets a bit differently + virtual const CKnownEntity * SelectMoreDangerousThreat( const INextBot *me, + const CBaseCombatCharacter *subject, + const CKnownEntity *threat1, + const CKnownEntity *threat2 ) const; // return the more dangerous of the two threats to 'subject', or NULL if we have no opinion + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + + virtual const char *GetName( void ) const { return "SniperLurk"; }; + +private: + CountdownTimer m_boredTimer; + CountdownTimer m_repathTimer; + PathFollower m_path; + int m_failCount; + + Vector m_homePosition; // where we want to snipe from + bool m_isHomePositionValid; + bool m_isAtHome; + bool FindNewHome( CTFBot *me ); + CountdownTimer m_findHomeTimer; + bool m_isOpportunistic; + + CUtlVector< CHandle< CTFBotHint > > m_hintVector; + CHandle< CTFBotHint > m_priorHint; + bool FindHint( CTFBot *me ); +}; + +#endif // TF_BOT_SNIPER_LURK_H diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.cpp new file mode 100644 index 0000000..8eab11c --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.cpp @@ -0,0 +1,412 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_attack.cpp +// Backstab or pistol, as appropriate +// Michael Booth, June 2010 + +#include "cbase.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/spy/tf_bot_spy_attack.h" +#include "bot/behavior/tf_bot_retreat_to_cover.h" +#include "bot/behavior/spy/tf_bot_spy_sap.h" + +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_spy_knife_range( "tf_bot_spy_knife_range", "300", FCVAR_CHEAT, "If threat is closer than this, prefer our knife" ); +ConVar tf_bot_spy_change_target_range_threshold( "tf_bot_spy_change_target_range_threshold", "300", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +CTFBotSpyAttack::CTFBotSpyAttack( CTFPlayer *victim ) : m_path( ChasePath::LEAD_SUBJECT ) +{ + m_victim = victim; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + m_isCoverBlown = false; + + if ( m_victim.Get() ) + { + me->GetVisionInterface()->AddKnownEntity( m_victim ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyAttack::Update( CTFBot *me, float interval ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetKnown( m_victim ); + + // opportunistically attack closer threat if they are much closer to us than our existing threat + const CKnownEntity *closestThreat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + + if ( !threat ) + { + threat = closestThreat; + m_isCoverBlown = false; + if ( closestThreat ) + { + m_victim = ToTFPlayer( closestThreat->GetEntity() ); + } + } + else if ( closestThreat && + closestThreat->GetEntity() && + closestThreat != threat ) + { + float rangeToCurrentThreat = me->GetRangeTo( threat->GetLastKnownPosition() ); + float rangeToNewThreat = me->GetRangeTo( closestThreat->GetLastKnownPosition() ); + + if ( rangeToCurrentThreat - rangeToNewThreat > tf_bot_spy_change_target_range_threshold.GetFloat() ) + { + if ( closestThreat->GetEntity()->IsPlayer() ) + { + threat = closestThreat; + m_victim = ToTFPlayer( closestThreat->GetEntity() ); + m_isCoverBlown = false; + } + } + } + + if ( !threat || threat->IsObsolete() ) + { + return Done( "No threat" ); + } + + + CBaseObject *sapTarget = me->GetNearestKnownSappableTarget(); + if ( sapTarget && me->IsEntityBetweenTargetAndSelf( sapTarget, threat->GetEntity() ) ) + { + return ChangeTo( new CTFBotSpySap( sapTarget ), "Opportunistically sapping an enemy object between my victim and I" ); + } + + if ( me->IsAnyEnemySentryAbleToAttackMe() ) + { + m_isCoverBlown = true; + + CBaseCombatWeapon *myGun = me->Weapon_GetWeaponByType( TF_WPN_TYPE_PRIMARY ); + me->Weapon_Switch( myGun ); + + return ChangeTo( new CTFBotRetreatToCover, "Escaping sentry fire!" ); + } + + CTFPlayer *playerThreat = ToTFPlayer( threat->GetEntity() ); + if ( !playerThreat ) + { + return Done( "Current 'threat' is not a player or a building?" ); + } + + // remember who we are attacking (in case we changed our minds) + m_victim = playerThreat; + + // uncloak so we can attack + if ( me->m_Shared.IsStealthed() && m_decloakTimer.IsElapsed() ) + { + me->PressAltFireButton(); + m_decloakTimer.Start( 1.0f ); + } + + bool isKnifeFight = false; + + if ( me->m_Shared.InCond( TF_COND_DISGUISED ) || + me->m_Shared.InCond( TF_COND_DISGUISING ) || + me->m_Shared.IsStealthed() ) + { + isKnifeFight = true; + } + + Vector playerThreatForward; + playerThreat->EyeVectors( &playerThreatForward ); + + Vector toPlayerThreat = playerThreat->GetAbsOrigin() - me->GetAbsOrigin(); + float threatRange = toPlayerThreat.NormalizeInPlace(); + + float behindTolerance = 0.0f; + + switch( me->GetDifficulty() ) + { + case CTFBot::EASY: behindTolerance = 0.9f; break; + case CTFBot::NORMAL: behindTolerance = 0.7071f; break; + case CTFBot::HARD: behindTolerance = 0.2f; break; + case CTFBot::EXPERT: behindTolerance = 0.0f; break; + } + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + behindTolerance = 0.7071f; + } + + bool isBehindVictim = DotProduct( playerThreatForward, toPlayerThreat ) > behindTolerance; + + // easy Spies always think they're in position to backstab + if ( me->GetDifficulty() == CTFBot::EASY ) + { + isBehindVictim = true; + } + + if ( threatRange < tf_bot_spy_knife_range.GetFloat() ) + { + isKnifeFight = true; + } + else if ( threat->IsVisibleInFOVNow() && isBehindVictim ) + { + // they are facing away from us - go for the backstab + isKnifeFight = true; + } + + // does my threat know I'm a Spy? + if ( me->IsThreatAimingTowardMe( playerThreat, 0.99f ) && me->GetTimeSinceLastInjury( GetEnemyTeam( me->GetTeamNumber() ) ) < 1.0f ) + { + m_isCoverBlown |= ( playerThreat->GetTimeSinceWeaponFired() < 0.25f ); + } + + if ( m_isCoverBlown || + me->m_Shared.InCond( TF_COND_BURNING ) || + me->m_Shared.InCond( TF_COND_URINE ) || + me->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) || + me->m_Shared.InCond( TF_COND_BLEEDING ) ) + { + isKnifeFight = false; + } + + CBaseCombatWeapon *myGun = me->Weapon_GetWeaponByType( isKnifeFight ? TF_WPN_TYPE_MELEE : TF_WPN_TYPE_PRIMARY ); + me->Weapon_Switch( myGun ); + + CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon(); + + bool isMovingTowardVictim = true; + + if ( myWeapon && myWeapon->IsMeleeWeapon() ) + { + if ( threat->IsVisibleInFOVNow() ) + { + const float circleStrafeRange = 250.0f; + + if ( threatRange < circleStrafeRange ) + { + // we're close - aim our stab attack + me->GetBodyInterface()->AimHeadTowards( playerThreat, IBody::MANDATORY, 0.1f, NULL, "Aiming my stab!" ); + + if ( !isBehindVictim ) + { + // circle around our victim to get behind them + Vector myForward; + me->EyeVectors( &myForward ); + + Vector cross; + CrossProduct( playerThreatForward, myForward, cross ); + + if ( cross.z < 0.0f ) + { + me->PressRightButton(); + } + else + { + me->PressLeftButton(); + } + + // don't continue to close in if we're already very close so we don't bump them and give ourselves away + if ( threatRange < 100.0f ) + { + isMovingTowardVictim = false; + } + } + else if ( TFGameRules()->IsMannVsMachineMode() ) + { + if ( m_chuckleTimer.IsElapsed() ) + { + m_chuckleTimer.Start( 1.0f ); + me->EmitSound( "Spy.MVM_Chuckle" ); + } + } + } + + if ( threatRange < me->GetDesiredAttackRange() ) + { + // if we're still disguised, go for the backstab + if ( me->m_Shared.InCond( TF_COND_DISGUISED ) ) + { + if ( isBehindVictim || m_isCoverBlown ) + { + // we're behind them (or they're onto us) - backstab! + me->PressFireButton(); + } + } + else + { + // we're exposed - stab! stab! stab! + me->PressFireButton(); + } + } + } + } + else + { + // aim our pistol + me->GetBodyInterface()->AimHeadTowards( playerThreat, IBody::MANDATORY, 0.1f, NULL, "Aiming my pistol" ); + } + + if ( isMovingTowardVictim ) + { + // pursue the threat. if not visible, go to the last known position + if ( !threat->IsVisibleRecently() || + me->IsRangeGreaterThan( threat->GetEntity()->GetAbsOrigin(), me->GetDesiredAttackRange() ) || + !me->IsLineOfFireClear( threat->GetEntity()->EyePosition() ) ) + { + // if we're at the threat's last known position and he's still not visible, we lost him + if ( !threat->IsVisibleRecently() ) + { + if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), 20.0f ) ) + { + me->GetVisionInterface()->ForgetEntity( threat->GetEntity() ); + return Done( "I lost my target!" ); + } + } + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Update( me, threat->GetEntity(), cost ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyAttack::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_victim = NULL; + m_path.Invalidate(); + m_isCoverBlown = false; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSpyAttack::OnStuck( CTFBot *me ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSpyAttack::OnInjured( CTFBot *me, const CTakeDamageInfo &info ) +{ + if ( me->IsEnemy( info.GetAttacker() ) ) + { + if ( !me->m_Shared.InCond( TF_COND_DISGUISED ) ) + { + // hurt by an enemy and exposed as a spy - flee! + m_isCoverBlown = true; + + CBaseCombatWeapon *myGun = me->Weapon_GetWeaponByType( TF_WPN_TYPE_PRIMARY ); + me->Weapon_Switch( myGun ); + + return TryChangeTo( new CTFBotRetreatToCover, RESULT_IMPORTANT, "Time to get out of here!" ); + } + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSpyAttack::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result ) +{ + if ( me->IsEnemy( other ) && other->MyCombatCharacterPointer() ) + { + if ( other->MyCombatCharacterPointer()->IsLookingTowards( me ) ) + { + m_isCoverBlown = true; + } + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSpyAttack::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSpyAttack::ShouldHurry( const INextBot *me ) const +{ + return ANSWER_YES; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSpyAttack::ShouldAttack( const INextBot *meBot, const CKnownEntity *them ) const +{ + CTFBot *me = ToTFBot( meBot->GetEntity() ); + + if ( m_isCoverBlown || + me->m_Shared.InCond( TF_COND_BURNING ) || + me->m_Shared.InCond( TF_COND_URINE ) || + me->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) || + me->m_Shared.InCond( TF_COND_BLEEDING ) ) + { + // our cover is blown anyway + return ANSWER_YES; + } + + return ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +// Use this to signal the enemy we are focusing on, so we dont avoid them +QueryResultType CTFBotSpyAttack::IsHindrance( const INextBot *me, CBaseEntity *blocker ) const +{ + if ( blocker != IS_ANY_HINDRANCE_POSSIBLE ) + { + if ( blocker && m_victim.Get() && blocker->entindex() == m_victim->entindex() ) + { + // don't avoid this guy + return ANSWER_NO; + } + } + + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +// Return the more dangerous of the two threats to 'subject', or NULL if we have no opinion +const CKnownEntity * CTFBotSpyAttack::SelectMoreDangerousThreat( const INextBot *meBot, + const CBaseCombatCharacter *subject, + const CKnownEntity *threat1, + const CKnownEntity *threat2 ) const +{ + CTFBot *me = ToTFBot( meBot->GetEntity() ); + + if ( me->IsSelf( subject ) ) + { + CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon(); + if ( myWeapon && myWeapon->IsMeleeWeapon() ) + { + // attack the closest victim with my knife + if ( me->GetRangeSquaredTo( threat1->GetEntity() ) < me->GetRangeSquaredTo( threat2->GetEntity() ) ) + { + return threat1; + } + + return threat2; + } + } + + return NULL; +} + diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.h new file mode 100644 index 0000000..085d043 --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_attack.h @@ -0,0 +1,49 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_attack.h +// Backstab or pistol, as appropriate +// Michael Booth, June 2010 + +#ifndef TF_BOT_SPY_ATTACK_H +#define TF_BOT_SPY_ATTACK_H + +#include "Path/NextBotChasePath.h" + + +//------------------------------------------------------------------------------- +class CTFBotSpyAttack : public Action< CTFBot > +{ +public: + CTFBotSpyAttack( CTFPlayer *victim ); + virtual ~CTFBotSpyAttack() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + virtual QueryResultType ShouldAttack( const INextBot *meBot, const CKnownEntity *them ) const; + virtual QueryResultType IsHindrance( const INextBot *me, CBaseEntity *blocker ) const; // use this to signal the enemy we are focusing on, so we dont avoid them + + virtual const CKnownEntity * SelectMoreDangerousThreat( const INextBot *me, + const CBaseCombatCharacter *subject, + const CKnownEntity *threat1, + const CKnownEntity *threat2 ) const; // return the more dangerous of the two threats to 'subject', or NULL if we have no opinion + + virtual const char *GetName( void ) const { return "SpyAttack"; }; + +private: + CHandle< CTFPlayer > m_victim; + ChasePath m_path; + bool m_isCoverBlown; + CountdownTimer m_chuckleTimer; + CountdownTimer m_decloakTimer; +}; + + +#endif // TF_BOT_SPY_ATTACK_H diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.cpp new file mode 100644 index 0000000..db5c69e --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.cpp @@ -0,0 +1,31 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_backstab.cpp +// Chase behind a victim and backstab them +// Michael Booth, June 2010 + +#include "cbase.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/spy/tf_bot_spy_backstab.h" + +extern ConVar tf_bot_path_lookahead_range; + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyBackstab::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyBackstab::Update( CTFBot *me, float interval ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSpyBackstab::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_NO; +} diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.h new file mode 100644 index 0000000..1efd0a0 --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_backstab.h @@ -0,0 +1,24 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_backstab.h +// Chase behind a victim and backstab them +// Michael Booth, June 2010 + +#ifndef TF_BOT_SPY_BACKSTAB_H +#define TF_BOT_SPY_BACKSTAB_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotSpyBackstab : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "SpyBackstab"; }; + +private: +}; + +#endif // TF_BOT_SPY_BACKSTAB_H diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.cpp new file mode 100644 index 0000000..828da5d --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.cpp @@ -0,0 +1,31 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_escape.cpp +// Flee! +// Michael Booth, June 2010 + +#include "cbase.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/spy/tf_bot_spy_escape.h" + +extern ConVar tf_bot_path_lookahead_range; + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyEscape::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyEscape::Update( CTFBot *me, float interval ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSpyEscape::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_NO; +} diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.h new file mode 100644 index 0000000..cfd887e --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_escape.h @@ -0,0 +1,24 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_escape.h +// Flee! +// Michael Booth, June 2010 + +#ifndef TF_BOT_SPY_ESCAPE_H +#define TF_BOT_SPY_ESCAPE_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotSpyEscape : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "SpyEscape"; }; + +private: +}; + +#endif // TF_BOT_SPY_ESCAPE_H diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.cpp new file mode 100644 index 0000000..2a35b31 --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.cpp @@ -0,0 +1,236 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_hide.cpp +// Move to a hiding spot +// Michael Booth, September 2011 + +#include "cbase.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/spy/tf_bot_spy_hide.h" +#include "bot/behavior/spy/tf_bot_spy_lurk.h" +#include "bot/behavior/spy/tf_bot_spy_attack.h" + + +//--------------------------------------------------------------------------------------------- +CTFBotSpyHide::CTFBotSpyHide( CTFPlayer *victim ) +{ + m_initialVictim = victim; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyHide::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_hidingSpot = NULL; + m_findTimer.Invalidate(); + m_isAtGoal = false; + + CTFNavArea *myArea = me->GetLastKnownArea(); + + int enemyTeam = GetEnemyTeam( me->GetTeamNumber() ); + + m_incursionThreshold = myArea ? myArea->GetIncursionDistance( enemyTeam ) : FLT_MAX; + if ( m_incursionThreshold < 0.0f ) + { + m_incursionThreshold = FLT_MAX; + } + + m_talkTimer.Start( RandomFloat( 5.0f, 10.0f ) ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyHide::Update( CTFBot *me, float interval ) +{ + if ( m_initialVictim != NULL && !me->GetVisionInterface()->IsIgnored( m_initialVictim ) ) + { + return SuspendFor( new CTFBotSpyAttack( m_initialVictim ), "Going after our initial victim" ); + } + + // go after victims we've gotten behind + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->GetTimeSinceLastKnown() < 3.0f ) + { + CTFPlayer *victim = ToTFPlayer( threat->GetEntity() ); + if ( victim ) + { + const float attackRange = 750.0f; + if ( me->IsRangeLessThan( victim, attackRange ) ) + { + if ( !victim->IsLookingTowards( me ) || victim->IsFiringWeapon() ) + { + return SuspendFor( new CTFBotSpyAttack( victim ), "Opportunistic attack or self defense!" ); + } + } + } + } + + if ( m_talkTimer.IsElapsed() ) + { + m_talkTimer.Start( RandomFloat( 5.0f, 10.0f ) ); + me->EmitSound( "Spy.TeaseVictim" ); + } + + if ( m_isAtGoal ) + { + // Quiet everyone! We are hiding now! + CTFNavArea *myArea = me->GetLastKnownArea(); + if ( myArea ) + { + int enemyTeam = GetEnemyTeam( me->GetTeamNumber() ); + + m_incursionThreshold = myArea->GetIncursionDistance( enemyTeam ); + } + + return SuspendFor( new CTFBotSpyLurk, "Reached hiding spot - lurking" ); + } + + if ( m_hidingSpot == NULL && m_findTimer.IsElapsed() ) + { + FindHidingSpot( me ); + } + + // move to our hiding spot + m_path.Update( me ); + + // path following may invalidate our hiding spot (OnMoveToFailure()) + if ( m_hidingSpot == NULL ) + { + return Continue(); + } + + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); + + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_path.Compute( me, m_hidingSpot->GetPosition(), cost ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyHide::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_hidingSpot = NULL; + m_isAtGoal = false; + m_initialVictim = NULL; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSpyHide::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + m_isAtGoal = true; + + return TryContinue( RESULT_CRITICAL ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSpyHide::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + m_hidingSpot = NULL; + m_isAtGoal = false; + + return TryContinue( RESULT_IMPORTANT ); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSpyHide::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_NO; +} + +struct IncursionEntry_t +{ + int team; + CTFNavArea *area; +}; + +//--------------------------------------------------------------------------------------------- +class SpyHideIncursionDistanceLess +{ +public: + bool Less( const IncursionEntry_t &src1, const IncursionEntry_t &src2, void *pCtx ) + { + return src1.area->GetIncursionDistance( src1.team ) < src2.area->GetIncursionDistance( src2.team ); + } +}; + + +//--------------------------------------------------------------------------------------------- +bool CTFBotSpyHide::FindHidingSpot( CTFBot *me ) +{ + CTFNavArea *myArea = me->GetLastKnownArea(); + if ( !myArea ) + { + return false; + } + + m_hidingSpot = NULL; + + // find a spot to hide + const float maxRange = 3500.0f; + CUtlVector< CNavArea * > nearbyVector; + CollectSurroundingAreas( &nearbyVector, me->GetLastKnownArea(), maxRange, + 500.0f, 500.0f ); + + CUtlSortVector< IncursionEntry_t, SpyHideIncursionDistanceLess > hidingSpotVector; + + float maxIncursion = m_incursionThreshold + 1000.0f; + + int enemyTeam = GetEnemyTeam( me->GetTeamNumber() ); + + // if we are standing in an area the defenders can't reach, don't limit + if ( myArea->GetIncursionDistance( enemyTeam ) < 0.0f ) + { + maxIncursion = 9999999; + } + + for( int i=0; i<nearbyVector.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)nearbyVector[i]; + + if ( area->GetHidingSpots()->Count() <= 0 ) + { + continue; + } + + if ( area->GetIncursionDistance( enemyTeam ) < 0 ) + { + continue; + } + + // keep pushing inwards towards defender's spawn + if ( area->GetIncursionDistance( enemyTeam ) > maxIncursion ) + { + continue; + } + + IncursionEntry_t entry = { enemyTeam, area }; + hidingSpotVector.Insert( entry ); + } + + if ( hidingSpotVector.Count() <= 0 ) + { + return false; + } + + // penetrate as far as we can + int which = RandomInt( 0, hidingSpotVector.Count()/2 ); + CTFNavArea *whichArea = hidingSpotVector[ which ].area; + + const HidingSpotVector *hidingSpots = whichArea->GetHidingSpots(); + + m_hidingSpot = hidingSpots->Element( RandomInt( 0, hidingSpots->Count()-1 ) ); + + return true; +} diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.h new file mode 100644 index 0000000..dedd670 --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_hide.h @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_hide.h +// Move to a hiding spot +// Michael Booth, September 2011 + +#ifndef TF_BOT_SPY_HIDE +#define TF_BOT_SPY_HIDE + +#include "Path/NextBotPathFollow.h" + +class CTFBotSpyHide : public Action< CTFBot > +{ +public: + CTFBotSpyHide( CTFPlayer *victim = NULL ); + virtual ~CTFBotSpyHide() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "SpyHide"; }; + +private: + CHandle< CTFPlayer > m_initialVictim; + + HidingSpot *m_hidingSpot; + bool FindHidingSpot( CTFBot *me ); + CountdownTimer m_findTimer; + + PathFollower m_path; + CountdownTimer m_repathTimer; + bool m_isAtGoal; + + float m_incursionThreshold; + + CountdownTimer m_talkTimer; +}; + +#endif // TF_BOT_SPY_HIDE diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.cpp new file mode 100644 index 0000000..ae0de20 --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.cpp @@ -0,0 +1,335 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_infiltrate.cpp +// Move into position behind enemy lines and wait for victims +// Michael Booth, June 2010 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_obj_sentrygun.h" +#include "bot/tf_bot.h" +#include "bot/behavior/spy/tf_bot_spy_infiltrate.h" +#include "bot/behavior/spy/tf_bot_spy_sap.h" +#include "bot/behavior/spy/tf_bot_spy_attack.h" +#include "bot/behavior/tf_bot_retreat_to_cover.h" + +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_debug_spy( "tf_bot_debug_spy", "0", FCVAR_CHEAT ); + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyInfiltrate::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_hideArea = NULL; + + m_hasEnteredCombatZone = false; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyInfiltrate::Update( CTFBot *me, float interval ) +{ + // switch to our pistol + CBaseCombatWeapon *myGun = me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ); + if ( myGun ) + { + me->Weapon_Switch( myGun ); + } + + CTFNavArea *myArea = me->GetLastKnownArea(); + + if ( !myArea ) + { + return Continue(); + } + + bool isInMySpawn = myArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ); + if ( myArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_EXIT ) ) + { + // don't count exits so we cloak as we leave + isInMySpawn = false; + } + + // cloak when we first enter an area of active combat + if ( !me->m_Shared.IsStealthed() && + !isInMySpawn && + myArea->IsInCombat() && + !m_hasEnteredCombatZone ) + { + m_hasEnteredCombatZone = true; + me->PressAltFireButton(); + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->GetEntity() && threat->GetEntity()->IsBaseObject() ) + { + CBaseObject *enemyObject = (CBaseObject *)threat->GetEntity(); + if ( !enemyObject->HasSapper() && me->IsEnemy( enemyObject ) ) + { + return SuspendFor( new CTFBotSpySap( enemyObject ), "Sapping an enemy object" ); + } + } + + if ( me->GetEnemySentry() && !me->GetEnemySentry()->HasSapper() ) + { + return SuspendFor( new CTFBotSpySap( me->GetEnemySentry() ), "Sapping a Sentry" ); + } + + if ( !m_hideArea && m_findHidingSpotTimer.IsElapsed() ) + { + FindHidingSpot( me ); + m_findHidingSpotTimer.Start( 3.0f ); + } + + if ( !TFGameRules()->InSetup() ) + { + // go after victims we've gotten behind + if ( threat && threat->GetTimeSinceLastKnown() < 3.0f ) + { + CTFPlayer *victim = ToTFPlayer( threat->GetEntity() ); + if ( victim ) + { + CTFNavArea *victimArea = (CTFNavArea *)victim->GetLastKnownArea(); + if ( victimArea ) + { + int victimTeam = victim->GetTeamNumber(); + + if ( victimArea->GetIncursionDistance( victimTeam ) > myArea->GetIncursionDistance( victimTeam ) ) + { + if ( me->m_Shared.IsStealthed() ) + { + return SuspendFor( new CTFBotRetreatToCover( new CTFBotSpyAttack( victim ) ), "Hiding to decloak before going after a backstab victim" ); + } + else + { + return SuspendFor( new CTFBotSpyAttack( victim ), "Going after a backstab victim" ); + } + } + } + } + } + } + + if ( m_hideArea ) + { + if ( tf_bot_debug_spy.GetBool() ) + { + m_hideArea->DrawFilled( 255, 255, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + + if ( myArea == m_hideArea ) + { + // stay hidden during setup time + if ( TFGameRules()->InSetup() ) + { + m_waitTimer.Start( RandomFloat( 0.0f, 5.0f ) ); + } + else + { + // wait in our hiding spot for a bit, then try another + if ( !m_waitTimer.HasStarted() ) + { + m_waitTimer.Start( RandomFloat( 5.0f, 10.0f ) ); + } + else if ( m_waitTimer.IsElapsed() ) + { + // time to find a new hiding spot + m_hideArea = NULL; + } + } + } + else + { + // move to our ambush position + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + // we may not be able to path to our hiding spot, but get as close as we can + // (dropdown mid spawn in cp_gorge) + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_path.Compute( me, m_hideArea->GetCenter(), cost ); + } + + m_path.Update( me ); + + m_waitTimer.Invalidate(); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotSpyInfiltrate::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyInfiltrate::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyInfiltrate::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_repathTimer.Invalidate(); + m_hideArea = NULL; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBotSpyInfiltrate::FindHidingSpot( CTFBot *me ) +{ + m_hideArea = NULL; + + if ( me->GetAliveDuration() < 5.0f && TFGameRules()->InSetup() ) + { + // wait a bit until the nav mesh has updated itself + return false; + } + + int myTeam = me->GetTeamNumber(); + const CUtlVector< CTFNavArea * > *enemySpawnExitVector = TheTFNavMesh()->GetSpawnRoomExitAreas( GetEnemyTeam( myTeam ) ); + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() ) + { + // for now, just lurk where we are + return false; + } +#endif + + if ( !enemySpawnExitVector || enemySpawnExitVector->Count() == 0 ) + { + if ( tf_bot_debug_spy.GetBool() ) + { + DevMsg( "%3.2f: No enemy spawn room exit areas found\n", gpGlobals->curtime ); + } + return false; + } + + // find nearby place to hide hear enemy spawn exit(s) + CUtlVector< CNavArea * > nearbyAreaVector; + const float nearbyHideRange = 2500.0f; + for( int x=0; x<enemySpawnExitVector->Count(); ++x ) + { + CTFNavArea *enemySpawnExitArea = enemySpawnExitVector->Element( x ); + + CUtlVector< CNavArea * > nearbyThisExitAreaVector; + CollectSurroundingAreas( &nearbyThisExitAreaVector, enemySpawnExitArea, nearbyHideRange, me->GetLocomotionInterface()->GetStepHeight(), me->GetLocomotionInterface()->GetStepHeight() ); + + // concat vectors (assuming N^2 unique search would cost more than ripping through some duplicates) + nearbyAreaVector.AddVectorToTail( nearbyThisExitAreaVector ); + } + + // find area not visible to any enemy spawn exits + CUtlVector< CTFNavArea * > hideAreaVector; + int i; + + for( i=0; i<nearbyAreaVector.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)nearbyAreaVector[i]; + + if ( !me->GetLocomotionInterface()->IsAreaTraversable( area ) ) + continue; + + bool isHidden = true; + for( int j=0; j<enemySpawnExitVector->Count(); ++j ) + { + if ( area->IsPotentiallyVisible( enemySpawnExitVector->Element(j) ) ) + { + isHidden = false; + break; + } + } + + if ( isHidden ) + { + hideAreaVector.AddToTail( area ); + } + } + + if ( hideAreaVector.Count() == 0 ) + { + if ( tf_bot_debug_spy.GetBool() ) + { + DevMsg( "%3.2f: Can't find any non-visible hiding areas, trying for anything near the spawn exit...\n", gpGlobals->curtime ); + } + + for( i=0; i<nearbyAreaVector.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)nearbyAreaVector[i]; + + if ( !me->GetLocomotionInterface()->IsAreaTraversable( area ) ) + continue; + + hideAreaVector.AddToTail( area ); + } + } + + if ( hideAreaVector.Count() == 0 ) + { + if ( tf_bot_debug_spy.GetBool() ) + { + DevMsg( "%3.2f: Can't find any areas near the enemy spawn exit - just heading to the enemy spawn and hoping...\n", gpGlobals->curtime ); + } + + m_hideArea = enemySpawnExitVector->Element( RandomInt( 0, enemySpawnExitVector->Count()-1 ) ); + + return false; + } + + // pick a specific hiding spot + m_hideArea = hideAreaVector[ RandomInt( 0, hideAreaVector.Count()-1 ) ]; + + return true; +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSpyInfiltrate::OnStuck( CTFBot *me ) +{ + m_hideArea = NULL; + m_findHidingSpotTimer.Invalidate(); + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSpyInfiltrate::OnTerritoryCaptured( CTFBot *me, int territoryID ) +{ + // enemy spawn likely changed - find new hiding spot after internal data has updated + m_hideArea = NULL; + m_findHidingSpotTimer.Start( 5.0f ); + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSpyInfiltrate::OnTerritoryLost( CTFBot *me, int territoryID ) +{ + // enemy spawn likely changed - find new hiding spot after internal data has updated + m_hideArea = NULL; + m_findHidingSpotTimer.Start( 5.0f ); + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSpyInfiltrate::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_NO; +} diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.h new file mode 100644 index 0000000..db0be5a --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_infiltrate.h @@ -0,0 +1,42 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_infiltrate.h +// Move into position behind enemy lines and wait for victims +// Michael Booth, June 2010 + +#ifndef TF_BOT_SPY_INFILTRATE_H +#define TF_BOT_SPY_INFILTRATE_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotSpyInfiltrate : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + virtual ActionResult< CTFBot > OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "SpyInfiltrate"; }; + +private: + CountdownTimer m_repathTimer; + PathFollower m_path; + + CTFNavArea *m_hideArea; + bool FindHidingSpot( CTFBot *me ); + CountdownTimer m_findHidingSpotTimer; + + CountdownTimer m_waitTimer; + + bool m_hasEnteredCombatZone; +}; + + +#endif // TF_BOT_SPY_INFILTRATE_H diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.cpp new file mode 100644 index 0000000..7ee3d9c --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.cpp @@ -0,0 +1,160 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_leave_spawn_room.cpp +// Assume the enemy is watching our spawn - escape it +// Michael Booth, September 2011 + +#include "cbase.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/spy/tf_bot_spy_leave_spawn_room.h" +#include "bot/behavior/spy/tf_bot_spy_hide.h" + + +extern bool IsSpaceToSpawnHere( const Vector &where ); + +//--------------------------------------------------------------------------------------------- +bool TeleportNearVictim( CTFBot *me, CTFPlayer *victim, int attempt ) +{ + VPROF_BUDGET( "CTFBotSpyLeaveSpawnRoom::TeleportNearVictim", "NextBot" ); + + if ( !victim ) + { + return false; + } + + CUtlVector< CTFNavArea * > ambushVector; // vector of hidden but near-to-victim areas + + if ( !victim->GetLastKnownArea() ) + { + return false; + } + + const float maxSurroundTravelRange = 6000.0f; + + float surroundTravelRange = 1500.0f + 500.0f * attempt; + if ( surroundTravelRange > maxSurroundTravelRange ) + { + surroundTravelRange = maxSurroundTravelRange; + } + + CUtlVector< CNavArea * > areaVector; + + // collect walkable areas surrounding this victim + CollectSurroundingAreas( &areaVector, victim->GetLastKnownArea(), surroundTravelRange, StepHeight, StepHeight ); + + // keep subset that isn't visible to the victim's team + for( int j=0; j<areaVector.Count(); ++j ) + { + CTFNavArea *area = (CTFNavArea *)areaVector[j]; + + if ( !area->IsValidForWanderingPopulation() ) + { + continue; + } + + if ( area->IsPotentiallyVisibleToTeam( victim->GetTeamNumber() ) ) + { + continue; + } + + ambushVector.AddToTail( area ); + } + + if ( ambushVector.Count() == 0 ) + { + return false; + } + + int maxTries = MIN( 10, ambushVector.Count() ); + + for( int retry=0; retry<maxTries; ++retry ) + { + int which = RandomInt( 0, ambushVector.Count()-1 ); + Vector where = ambushVector[ which ]->GetCenter() + Vector( 0, 0, StepHeight ); + + if ( IsSpaceToSpawnHere( where ) ) + { + me->Teleport( &where, &vec3_angle, &vec3_origin ); + return true; + } + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyLeaveSpawnRoom::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + // disguise as enemy team + me->DisguiseAsMemberOfEnemyTeam(); + + // cloak + me->PressAltFireButton(); + + // wait a few moments to guarantee a minimum time between announcing Spies and their attack + m_waitTimer.Start( 2.0f + RandomFloat( 0.0f, 1.0f ) ); + + m_attempt = 0; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyLeaveSpawnRoom::Update( CTFBot *me, float interval ) +{ + VPROF_BUDGET( "CTFBotSpyLeaveSpawnRoom::Update", "NextBot" ); + + if ( m_waitTimer.IsElapsed() ) + { + CTFPlayer *victim = NULL; + + CUtlVector< CTFPlayer * > enemyVector; + CollectPlayers( &enemyVector, GetEnemyTeam( me->GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS ); + + // randomly shuffle our enemies + CUtlVector< CTFPlayer * > shuffleVector; + shuffleVector = enemyVector; + int n = shuffleVector.Count(); + while( n > 1 ) + { + int k = RandomInt( 0, n-1 ); + n--; + + CTFPlayer *tmp = shuffleVector[n]; + shuffleVector[n] = shuffleVector[k]; + shuffleVector[k] = tmp; + } + + for( int i=0; i<shuffleVector.Count(); ++i ) + { + if ( TeleportNearVictim( me, shuffleVector[i], m_attempt ) ) + { + victim = shuffleVector[i]; + break; + } + } + + // if we didn't find a victim, try again in a bit + if ( !victim ) + { + m_waitTimer.Start( 1.0f ); + + ++m_attempt; + + return Continue(); + } + + return ChangeTo( new CTFBotSpyHide( victim ), "Hiding!" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSpyLeaveSpawnRoom::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_NO; +} diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.h new file mode 100644 index 0000000..3471749 --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_leave_spawn_room.h @@ -0,0 +1,26 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_leave_spawn_room.h +// Assume the enemy is watching our spawn - escape it +// Michael Booth, September 2011 + +#ifndef TF_BOT_LEAVE_SPAWN_ROOM_H +#define TF_BOT_LEAVE_SPAWN_ROOM_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotSpyLeaveSpawnRoom : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "SpyLeaveSpawnRoom"; }; + +private: + CountdownTimer m_waitTimer; + int m_attempt; +}; + +#endif // TF_BOT_LEAVE_SPAWN_ROOM_H diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.cpp new file mode 100644 index 0000000..a9379be --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.cpp @@ -0,0 +1,91 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_lurk.cpp +// Wait for victims +// Michael Booth, September 2011 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_obj_sentrygun.h" +#include "bot/tf_bot.h" +#include "bot/behavior/spy/tf_bot_spy_lurk.h" +#include "bot/behavior/spy/tf_bot_spy_sap.h" +#include "bot/behavior/spy/tf_bot_spy_attack.h" +#include "bot/behavior/tf_bot_retreat_to_cover.h" +#include "bot/behavior/spy/tf_bot_spy_sap.h" + +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; +extern ConVar tf_bot_debug_spy; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyLurk::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + // cloak + if ( !me->m_Shared.IsStealthed() ) + { + me->PressAltFireButton(); + } + + // disguise as the enemy team + me->DisguiseAsMemberOfEnemyTeam(); + + m_lurkTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpyLurk::Update( CTFBot *me, float interval ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->GetEntity() ) + { + CBaseObject *enemyObject = dynamic_cast< CBaseObject * >( threat->GetEntity() ); + if ( enemyObject && !enemyObject->HasSapper() && me->IsEnemy( enemyObject ) ) + { + return SuspendFor( new CTFBotSpySap( enemyObject ), "Sapping an enemy object" ); + } + } + + if ( me->GetEnemySentry() != NULL && !me->GetEnemySentry()->HasSapper() ) + { + return SuspendFor( new CTFBotSpySap( me->GetEnemySentry() ), "Sapping a Sentry" ); + } + + if ( m_lurkTimer.IsElapsed() ) + { + return Done( "Lost patience with my hiding spot" ); + } + + CTFNavArea *myArea = me->GetLastKnownArea(); + + if ( !myArea ) + { + return Continue(); + } + + // go after victims we've gotten behind + if ( threat && threat->GetTimeSinceLastKnown() < 3.0f ) + { + CTFPlayer *victim = ToTFPlayer( threat->GetEntity() ); + if ( victim ) + { + if ( !victim->IsLookingTowards( me ) ) + { + return ChangeTo( new CTFBotSpyAttack( victim ), "Going after a backstab victim" ); + } + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSpyLurk::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_NO; +} diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.h new file mode 100644 index 0000000..73e2d44 --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_lurk.h @@ -0,0 +1,26 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_lurk.h +// Wait for victims +// Michael Booth, September 2011 + +#ifndef TF_BOT_SPY_LURK_H +#define TF_BOT_SPY_LURK_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotSpyLurk : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "SpyLurk"; }; + +private: + CountdownTimer m_lurkTimer; +}; + + +#endif // TF_BOT_SPY_LURK_H diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.cpp b/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.cpp new file mode 100644 index 0000000..776339a --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.cpp @@ -0,0 +1,250 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_sap.cpp +// Sap nearby enemy buildings +// Michael Booth, June 2010 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_obj_sentrygun.h" +#include "bot/tf_bot.h" +#include "bot/behavior/spy/tf_bot_spy_sap.h" +#include "bot/behavior/tf_bot_approach_object.h" +#include "bot/behavior/spy/tf_bot_spy_attack.h" + +extern ConVar tf_bot_path_lookahead_range; +extern ConVar tf_bot_debug_spy; + + +//--------------------------------------------------------------------------------------------- +CTFBotSpySap::CTFBotSpySap( CBaseObject *sapTarget ) +{ + m_sapTarget = sapTarget; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpySap::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + me->StopLookingAroundForEnemies(); + + // uncloak so we can sap + if ( me->m_Shared.IsStealthed() ) + { + me->PressAltFireButton(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpySap::Update( CTFBot *me, float interval ) +{ + CBaseObject *newSapTarget = me->GetNearestKnownSappableTarget(); + + if ( newSapTarget ) + { + m_sapTarget = newSapTarget; + } + + if ( m_sapTarget == NULL ) + { + return Done( "Sap target gone" ); + } + + CTFPlayer *victim = NULL; + + CUtlVector< CKnownEntity > knownVector; + me->GetVisionInterface()->CollectKnownEntities( &knownVector ); + + for( int i=0; i<knownVector.Count(); ++i ) + { + CTFPlayer *playerThreat = ToTFPlayer( knownVector[i].GetEntity() ); + if ( playerThreat && me->IsEnemy( playerThreat ) ) + { + victim = playerThreat; + break; + } + } + + // opportunistic backstab if engineer is between me and my sap target + if ( victim && victim->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + const float nearbyRange = 150.0f; + if ( m_sapTarget->GetOwner() == victim && me->IsRangeLessThan( victim, nearbyRange ) ) + { + if ( me->IsEntityBetweenTargetAndSelf( victim, m_sapTarget ) ) + { + return SuspendFor( new CTFBotSpyAttack( victim ), "Backstabbing the engineer before I sap his buildings" ); + } + } + } + + const float sapRange = 40.0f; + + if ( me->IsRangeLessThan( m_sapTarget, 2.0f * sapRange ) ) + { + // switch to our sapper and spam it + CBaseCombatWeapon *mySapper = me->Weapon_GetWeaponByType( TF_WPN_TYPE_BUILDING ); + if ( !mySapper ) + { + return Done( "I have no sapper" ); + } + + me->Weapon_Switch( mySapper ); + + // uncloak + if ( me->m_Shared.IsStealthed() ) + { + me->PressAltFireButton(); + } + + // sap our target + me->GetBodyInterface()->AimHeadTowards( m_sapTarget, IBody::MANDATORY, 0.1f, NULL, "Aiming my sapper" ); + + me->PressFireButton(); + } + + if ( me->IsRangeGreaterThan( m_sapTarget, sapRange ) ) + { + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + if ( m_path.Compute( me, m_sapTarget, cost ) == false ) + { + return Done( "No path to sap target!" ); + } + } + + m_path.Update( me ); + + return Continue(); + } + + // if our target is sapped, look for other nearby buildings to sap + if ( m_sapTarget->HasSapper() ) + { + CBaseObject *nextTarget = me->GetNearestKnownSappableTarget(); + if ( nextTarget ) + { + m_sapTarget = nextTarget; + } + else + { + // everything is sapped - explicitly attack nearby enemy Engineers + if ( victim && victim->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + return SuspendFor( new CTFBotSpyAttack( victim ), "Attacking an engineer" ); + } + + return Done( "All targets sapped" ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotSpySap::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + me->StartLookingAroundForEnemies(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpySap::OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + me->StartLookingAroundForEnemies(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSpySap::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + me->StopLookingAroundForEnemies(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSpySap::OnStuck( CTFBot *me ) +{ + return TryDone( RESULT_CRITICAL, "I'm stuck, probably on a sapped building that hasn't exploded yet" ); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSpySap::ShouldAttack( const INextBot *meBot, const CKnownEntity *them ) const +{ + CTFBot *me = ToTFBot( meBot->GetEntity() ); + + if ( m_sapTarget && !m_sapTarget->HasSapper() ) + { + // mission not accomplished + return ANSWER_NO; + } + + if ( !me->m_Shared.InCond( TF_COND_DISGUISED ) && + !me->m_Shared.InCond( TF_COND_DISGUISING ) && + !me->m_Shared.IsStealthed() ) + { + // our cover is blown! + return ANSWER_YES; + } + + // if we've sapped, attack + return AreAllDangerousSentriesSapped( me ) ? ANSWER_YES : ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +// Don't avoid enemies when we're going in for the sap +QueryResultType CTFBotSpySap::IsHindrance( const INextBot *me, CBaseEntity *blocker ) const +{ + if ( m_sapTarget.Get() && me->IsRangeLessThan( m_sapTarget, 300.0f ) ) + { + // we're almost to our sap target - don't avoid anyone + return ANSWER_NO; + } + + // avoid everyone while we move to our sap target + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSpySap::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBotSpySap::AreAllDangerousSentriesSapped( CTFBot *me ) const +{ + CUtlVector< CKnownEntity > knownVector; + me->GetVisionInterface()->CollectKnownEntities( &knownVector ); + + for( int i=0; i<knownVector.Count(); ++i ) + { + CBaseObject *enemyObject = dynamic_cast< CBaseObject * >( knownVector[i].GetEntity() ); + if ( enemyObject && enemyObject->ObjectType() == OBJ_SENTRYGUN && !enemyObject->HasSapper() && me->IsEnemy( enemyObject ) ) + { + // this is an active enemy sentry, are we in range and line of fire? + if ( me->IsRangeLessThan( enemyObject, SENTRY_MAX_RANGE ) && me->IsLineOfFireClear( enemyObject ) ) + { + return false; + } + } + } + + return true; +} + + diff --git a/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.h b/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.h new file mode 100644 index 0000000..4c8fe77 --- /dev/null +++ b/game/server/tf/bot/behavior/spy/tf_bot_spy_sap.h @@ -0,0 +1,42 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_spy_sap.h +// Sap nearby enemy buildings +// Michael Booth, June 2010 + +#ifndef TF_BOT_SPY_SAP_H +#define TF_BOT_SPY_SAP_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotSpySap : public Action< CTFBot > +{ +public: + CTFBotSpySap( CBaseObject *sapTarget ); + virtual ~CTFBotSpySap() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual ActionResult< CTFBot > OnSuspend( CTFBot *me, Action< CTFBot > *interruptingAction ); + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType IsHindrance( const INextBot *me, CBaseEntity *blocker ) const; // use this to signal the enemy we are focusing on, so we dont avoid them + + virtual const char *GetName( void ) const { return "SpySap"; }; + +private: + CHandle< CBaseObject > m_sapTarget; + + CountdownTimer m_repathTimer; + PathFollower m_path; + + CBaseObject *GetNearestKnownSappableTarget( CTFBot *me ); + bool AreAllDangerousSentriesSapped( CTFBot *me ) const; +}; + +#endif // TF_BOT_SPY_SAP_H diff --git a/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.cpp b/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.cpp new file mode 100644 index 0000000..873762d --- /dev/null +++ b/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.cpp @@ -0,0 +1,336 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_escort_squad_leader.cpp +// Escort the squad leader to their destination +// Michael Booth, Octoboer 2011 + +#include "cbase.h" + +#include "bot/tf_bot.h" +#include "bot/behavior/squad/tf_bot_escort_squad_leader.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h" + +ConVar tf_bot_squad_escort_range( "tf_bot_squad_escort_range", "500", FCVAR_CHEAT ); +ConVar tf_bot_formation_debug( "tf_bot_formation_debug", "0", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +CTFBotEscortSquadLeader::CTFBotEscortSquadLeader( Action< CTFBot > *actionToDoAfterSquadDisbands ) // : m_path( ChasePath::LEAD_SUBJECT ) +{ + m_actionToDoAfterSquadDisbands = actionToDoAfterSquadDisbands; + m_formationPath.SetGoalTolerance( 0.0f ); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEscortSquadLeader::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_formationForward = vec3_origin; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEscortSquadLeader::Update( CTFBot *me, float interval ) +{ + if ( interval <= 0.0f ) + { + return Continue(); + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + // prepare to fight + me->EquipBestWeaponForThreat( threat ); + } + + CTFBotSquad *squad = me->GetSquad(); + if ( !squad ) + { + if ( m_actionToDoAfterSquadDisbands ) + { + return ChangeTo( m_actionToDoAfterSquadDisbands, "Not in a Squad" ); + } + + return Done( "Not in a Squad" ); + } + + // we need to update every tick to smoothly move in formation + me->FlagForUpdate(); + + CTFBot *leader = squad->GetLeader(); + if ( !leader || !leader->IsAlive() ) + { + me->LeaveSquad(); + + if ( m_actionToDoAfterSquadDisbands ) + { + return ChangeTo( m_actionToDoAfterSquadDisbands, "Squad leader is dead" ); + } + + return Done( "Squad leader is dead" ); + } + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && leader == me ) + { + const char* pszNowLeader = "I'm now the squad leader! Going for the flag!"; + if ( me->HasAttribute( CTFBot::AGGRESSIVE ) ) + { + // push for the point first, then attack + return ChangeTo( new CTFBotPushToCapturePoint( new CTFBotFetchFlag ), pszNowLeader ); + } + + // capture the flag + return ChangeTo( new CTFBotFetchFlag, pszNowLeader ); + } + + // if we're using a melee weapon, close and attack with it while staying near the leader + CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon(); + if ( myWeapon && myWeapon->IsMeleeWeapon() ) + { + if ( me->IsRangeLessThan( leader, tf_bot_squad_escort_range.GetFloat() ) && me->IsLineOfSightClear( leader ) ) + { + ActionResult< CTFBot > result = m_meleeAttackAction.Update( me, interval ); + + if ( result.IsContinue() ) + { + // we have a melee target, and we're still reasonably close to the flag leader + return Continue(); + } + } + } + + CUtlVector< CTFBot * > rawMemberVector; + squad->CollectMembers( &rawMemberVector ); + + // cull out the medics - they do their own thing + CUtlVector< CTFBot * > memberVector; + for( int m=0; m<rawMemberVector.Count(); ++m ) + { + if ( !rawMemberVector[m]->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + memberVector.AddToTail( rawMemberVector[m] ); + } + } + + const PathFollower *leaderPath = leader->GetCurrentPath(); + if ( !leaderPath || !leaderPath->GetCurrentGoal() ) + { + // no path, no formation + me->SetSquadFormationError( 0.0f ); + me->SetBrokenFormation( false ); + return Continue(); + } + + const Path::Segment *leaderSegment = leaderPath->GetCurrentGoal(); + + Vector leaderForward = leaderSegment->pos - leader->GetAbsOrigin(); + + // if the leader is very close to the goal, use the next goal to ensure + // the forward vector stays forward + const float atGoal = 25.0f; + if ( leaderForward.IsLengthLessThan( atGoal ) ) + { + const Path::Segment *nextSegment = leaderPath->NextSegment( leaderSegment ); + if ( nextSegment ) + { + leaderForward = nextSegment->pos - leader->GetAbsOrigin(); + } + } + + leaderForward.NormalizeInPlace(); + + if ( m_formationForward.IsZero() ) + { + m_formationForward = leaderForward; + } + else + { + // limit rate of change of leader forward vector to keep formation coherent + float maxRotation = 30.0f; // degrees/second + + float leaderForwardYaw = UTIL_VecToYaw( leaderForward ); + float formationYaw = UTIL_VecToYaw( m_formationForward ); + + float angleDiff = UTIL_AngleDiff( leaderForwardYaw, formationYaw ); + + float deltaYaw = maxRotation * interval; + + if ( angleDiff < -deltaYaw ) + { + formationYaw -= deltaYaw; + } + else if ( angleDiff > deltaYaw ) + { + formationYaw += deltaYaw; + } + else + { + formationYaw += angleDiff; + } + + FastSinCos( formationYaw * M_PI / 180.0f, &m_formationForward.y, &m_formationForward.x ); + m_formationForward.z = 0.0f; + } + + + const float maxSeparationAngle = 30.0f * M_PI / 180.0f; + + float formationRadius = 125.0f; + if ( squad->GetFormationSize() > 0.0f ) + { + formationRadius = squad->GetFormationSize(); + } + + Vector myFormationSpot; + Vector formationForward = vec3_origin; + float s, c; + + // where am I in the roster + int which; + for( which=0; which<memberVector.Count(); ++which ) + { + if ( me->IsSelf( memberVector[which] ) ) + { + break; + } + } + + // subtract one since the leader is always first + --which; + + // my formation spot is assigned via my position in the roster array + int slot = ( which + 1 ) /2; + + float formationAngle = slot * maxSeparationAngle; + + if ( which & 0x1 ) + { + formationAngle = -formationAngle; + } + + FastSinCos( formationAngle, &s, &c ); + formationForward.x = m_formationForward.x * c - m_formationForward.y * s; + formationForward.y = m_formationForward.y * c + m_formationForward.x * s; + + myFormationSpot = leader->GetAbsOrigin() + formationRadius * formationForward; + + trace_t result; + CTraceFilterIgnoreTeammates filter( me, COLLISION_GROUP_NONE, me->GetTeamNumber() ); + UTIL_TraceLine( leader->GetAbsOrigin() + Vector( 0, 0, HalfHumanHeight ), myFormationSpot + Vector( 0, 0, HalfHumanHeight ), MASK_PLAYERSOLID, &filter, &result ); + + if ( result.DidHitWorld() ) + { + myFormationSpot = result.endpos - Vector( 0, 0, HalfHumanHeight ) + 0.6f * me->GetBodyInterface()->GetHullWidth() * result.plane.normal; + } + + + if ( tf_bot_formation_debug.GetBool() ) + { + NDebugOverlay::Circle( myFormationSpot, 16.0f, 0, 255, 0, 255, true, 0.1f ); + + CFmtStr msg; + NDebugOverlay::Text( myFormationSpot, msg.sprintf( "%d", which ), false, 0.1f ); + } + + // match speed with leader if I'm at/near my formation position + Vector to = myFormationSpot - me->GetAbsOrigin(); + float error = to.Length2D(); + const float maxError = 100.0f; // 50 + + float normalizedError = 1.0f; + if ( error < maxError ) + { + normalizedError = error / maxError; + } + + // this error term is used in CTFPlayer::TeamFortress_CalculateMaxSpeed() to + // modulate our speed + // 0 = in position (no error) + // 1 = far out of position (max error) + me->SetSquadFormationError( normalizedError ); + + // move to my formation spot + if ( error < 50.0f ) + { + // if we're ahead of where we want to be, just wait + if ( DotProduct( to, formationForward ) > 0.0f ) + { + // very close - just directly approach to avoid pathing jaggies + me->GetLocomotionInterface()->Approach( myFormationSpot ); + } + else + { + // we're in position + me->SetSquadFormationError( 0.0f ); + } + } + else + { + if ( m_pathTimer.IsElapsed() ) + { + m_pathTimer.Start( RandomFloat( 0.1f, 0.2f ) ); + + me->SetBrokenFormation( false ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + if ( m_formationPath.Compute( me, myFormationSpot, cost ) == false ) + { + // no path back to formation + me->SetBrokenFormation( true ); + } + + // if we have a long path to get back in formation, we've broken ranks + const float tooFar = 750.0f; + if ( m_formationPath.GetLength() > tooFar ) + { + me->SetBrokenFormation( true ); + } + } + + m_formationPath.Update( me ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotEscortSquadLeader::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotWaitForOutOfPositionSquadMember::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_waitTimer.Start( 2.0f ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotWaitForOutOfPositionSquadMember::Update( CTFBot *me, float interval ) +{ + if ( m_waitTimer.IsElapsed() ) + { + return Done( "Timeout" ); + } + + if ( !me->IsInASquad() || !me->GetSquad()->IsLeader( me ) ) + { + return Done( "No squad" ); + } + + if ( me->GetSquad()->IsInFormation() ) + { + // Everyone is in position + return Done( "Everyone is in formation. Moving on." ); + } + + return Continue(); +} diff --git a/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.h b/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.h new file mode 100644 index 0000000..c9bf2cb --- /dev/null +++ b/game/server/tf/bot/behavior/squad/tf_bot_escort_squad_leader.h @@ -0,0 +1,55 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_escort_squad_leader.h +// Escort the squad leader to their destination +// Michael Booth, Octoboer 2011 + +#ifndef TF_BOT_ESCORT_SQUAD_LEADER_H +#define TF_BOT_ESCORT_SQUAD_LEADER_H + + +#include "Path/NextBotPathFollow.h" +#include "bot/behavior/tf_bot_melee_attack.h" + + +//----------------------------------------------------------------------------- +class CTFBotEscortSquadLeader : public Action< CTFBot > +{ +public: + CTFBotEscortSquadLeader( Action< CTFBot > *actionToDoAfterSquadDisbands = NULL ); + virtual ~CTFBotEscortSquadLeader() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual const char *GetName( void ) const { return "EscortSquadLeader"; }; + +private: + Action< CTFBot > *m_actionToDoAfterSquadDisbands; + CTFBotMeleeAttack m_meleeAttackAction; + + PathFollower m_formationPath; + CountdownTimer m_pathTimer; + + const Vector &GetFormationForwardVector( CTFBot *me ); + Vector m_formationForward; +}; + + +//----------------------------------------------------------------------------- +class CTFBotWaitForOutOfPositionSquadMember : public Action< CTFBot > +{ +public: + virtual ~CTFBotWaitForOutOfPositionSquadMember() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "WaitForOutOfPositionSquadMember"; }; + +private: + CountdownTimer m_waitTimer; +}; + + +#endif // TF_BOT_ESCORT_SQUAD_LEADER_H diff --git a/game/server/tf/bot/behavior/tf_bot_approach_object.cpp b/game/server/tf/bot/behavior/tf_bot_approach_object.cpp new file mode 100644 index 0000000..d2ebf63 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_approach_object.cpp @@ -0,0 +1,69 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_approach_object.h +// Move near/onto an object +// Michael Booth, February 2009 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_approach_object.h" + +extern ConVar tf_bot_path_lookahead_range; + +//--------------------------------------------------------------------------------------------- +CTFBotApproachObject::CTFBotApproachObject( CBaseEntity *loot, float range ) +{ + m_loot = loot; + m_range = range; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotApproachObject::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotApproachObject::Update( CTFBot *me, float interval ) +{ + if ( m_loot == NULL ) + { + return Done( "Object is NULL" ); + } + + if ( m_loot->IsEffectActive( EF_NODRAW ) ) + { + return Done( "Object is NODRAW" ); + } + + if ( me->GetLocomotionInterface()->GetGround() == m_loot ) + { + return Done( "I'm standing on the object" ); + } + + if ( me->IsDistanceBetweenLessThan( m_loot->GetAbsOrigin(), m_range ) ) + { + // in case we can't pick up the loot for some reason + return Done( "Reached object" ); + } + + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, m_loot->GetAbsOrigin(), cost ); + } + + // move to the loot + m_path.Update( me ); + + return Continue(); +} + + diff --git a/game/server/tf/bot/behavior/tf_bot_approach_object.h b/game/server/tf/bot/behavior/tf_bot_approach_object.h new file mode 100644 index 0000000..d19f4bc --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_approach_object.h @@ -0,0 +1,29 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_approach_object.h +// Move near/onto an object +// Michael Booth, February 2009 + +#ifndef TF_BOT_APPROACH_OBJECT_H +#define TF_BOT_APPROACH_OBJECT_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotApproachObject : public Action< CTFBot > +{ +public: + CTFBotApproachObject( CBaseEntity *loot, float range = 10.0f ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "ApproachObject"; }; + +private: + CHandle< CBaseEntity > m_loot; // what we are collecting + float m_range; // how close should we get + PathFollower m_path; // how we get to the loot + CountdownTimer m_repathTimer; +}; + + +#endif // TF_BOT_APPROACH_OBJECT_H diff --git a/game/server/tf/bot/behavior/tf_bot_attack.cpp b/game/server/tf/bot/behavior/tf_bot_attack.cpp new file mode 100644 index 0000000..98266b8 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_attack.cpp @@ -0,0 +1,157 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_attack.cpp +// Attack our threat +// Michael Booth, February 2009 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "team_control_point_master.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_attack.h" + +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; +extern ConVar tf_bot_offense_must_push_time; + + +//--------------------------------------------------------------------------------------------- +CTFBotAttack::CTFBotAttack( void ) : m_chasePath( ChasePath::LEAD_SUBJECT ) +{ +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +// head aiming and weapon firing is handled elsewhere - we just need to get into position to fight +ActionResult< CTFBot > CTFBotAttack::Update( CTFBot *me, float interval ) +{ + CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon(); + bool isUsingCloseRangeWeapon = ( myWeapon && ( myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) || myWeapon->IsMeleeWeapon() ) ); + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat == NULL || threat->IsObsolete() || !me->GetIntentionInterface()->ShouldAttack( me, threat ) ) + { + return Done( "No threat" ); + } + + me->EquipBestWeaponForThreat( threat ); + + if ( isUsingCloseRangeWeapon && threat->IsVisibleRecently() && me->IsRangeLessThan( threat->GetLastKnownPosition(), 1.1f * me->GetDesiredAttackRange() ) ) + { + // circle around our victim + if ( me->TransientlyConsistentRandomValue( 3.0f ) < 0.5f ) + { + me->PressLeftButton(); + } + else + { + me->PressRightButton(); + } + } + + + // pursue the threat. if not visible, go to the last known position + if ( !threat->IsVisibleRecently() || + me->IsRangeGreaterThan( threat->GetEntity()->GetAbsOrigin(), me->GetDesiredAttackRange() ) || + !me->IsLineOfFireClear( threat->GetEntity()->EyePosition() ) ) + { + if ( threat->IsVisibleRecently() ) + { + if ( isUsingCloseRangeWeapon && !TFGameRules()->IsMannVsMachineMode() ) // all bots in MvM use the default route + { + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_chasePath.Update( me, threat->GetEntity(), cost ); + } + else + { + CTFBotPathCost cost( me, DEFAULT_ROUTE ); + m_chasePath.Update( me, threat->GetEntity(), cost ); + } + } + else + { + // if we're at the threat's last known position and he's still not visible, we lost him + m_chasePath.Invalidate(); + + if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), 20.0f ) ) + { + me->GetVisionInterface()->ForgetEntity( threat->GetEntity() ); + return Done( "I lost my target!" ); + } + + // look where we last saw him as we approach + if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), me->GetMaxAttackRange() ) ) + { + me->GetBodyInterface()->AimHeadTowards( threat->GetLastKnownPosition() + Vector( 0, 0, HumanEyeHeight ), IBody::IMPORTANT, 0.2f, NULL, "Looking towards where we lost sight of our victim" ); + } + + m_path.Update( me ); + + if ( m_repathTimer.IsElapsed() ) + { + //m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); + m_repathTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + + if ( isUsingCloseRangeWeapon && !TFGameRules()->IsMannVsMachineMode() ) // all bots in MvM use the default route + { + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_path.Compute( me, threat->GetLastKnownPosition(), cost ); + } + else + { + CTFBotPathCost cost( me, DEFAULT_ROUTE ); + float maxPathLength = TFGameRules()->IsMannVsMachineMode() ? TFBOT_MVM_MAX_PATH_LENGTH : 0.0f; + m_path.Compute( me, threat->GetLastKnownPosition(), cost, maxPathLength ); + } + } + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotAttack::OnStuck( CTFBot *me ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotAttack::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotAttack::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotAttack::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotAttack::ShouldHurry( const INextBot *me ) const +{ + return ANSWER_UNDEFINED; +} + diff --git a/game/server/tf/bot/behavior/tf_bot_attack.h b/game/server/tf/bot/behavior/tf_bot_attack.h new file mode 100644 index 0000000..9f97acd --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_attack.h @@ -0,0 +1,38 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_attack.h +// Attack a threat +// Michael Booth, February 2009 + +#ifndef TF_BOT_ATTACK_H +#define TF_BOT_ATTACK_H + +#include "Path/NextBotChasePath.h" + + +//------------------------------------------------------------------------------- +class CTFBotAttack : public Action< CTFBot > +{ +public: + CTFBotAttack( void ); + virtual ~CTFBotAttack() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + + virtual const char *GetName( void ) const { return "Attack"; }; + +private: + PathFollower m_path; + ChasePath m_chasePath; + CountdownTimer m_repathTimer; +}; + + +#endif // TF_BOT_ATTACK_H diff --git a/game/server/tf/bot/behavior/tf_bot_behavior.cpp b/game/server/tf/bot/behavior/tf_bot_behavior.cpp new file mode 100644 index 0000000..08bf424 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_behavior.cpp @@ -0,0 +1,1649 @@ +//========= 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(); + } + } + } +} + diff --git a/game/server/tf/bot/behavior/tf_bot_behavior.h b/game/server/tf/bot/behavior/tf_bot_behavior.h new file mode 100644 index 0000000..2a09159 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_behavior.h @@ -0,0 +1,76 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_behavior.h +// Team Fortress NextBot behaviors +// Michael Booth, February 2009 + +#ifndef TF_BOT_BEHAVIOR_H +#define TF_BOT_BEHAVIOR_H + +#include "Path/NextBotPathFollow.h" + +class CTFBotMainAction : public Action< CTFBot > +{ +public: + virtual Action< CTFBot > *InitialContainedAction( CTFBot *me ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnKilled( CTFBot *me, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CTFBot > OnInjured( CTFBot *me, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL ); + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + + virtual EventDesiredResult< CTFBot > OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ); + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + + virtual Vector SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const; // given a subject, return the world space position we should aim at + virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const; + + virtual const CKnownEntity * SelectMoreDangerousThreat( const INextBot *me, + const CBaseCombatCharacter *subject, + const CKnownEntity *threat1, + const CKnownEntity *threat2 ) const; // return the more dangerous of the two threats to 'subject', or NULL if we have no opinion + + virtual const char *GetName( void ) const { return "MainAction"; }; + +private: + CountdownTimer m_reloadTimer; + mutable CountdownTimer m_aimAdjustTimer; + mutable float m_aimErrorRadius; + mutable float m_aimErrorAngle; + + float m_yawRate; + float m_priorYaw; + IntervalTimer m_steadyTimer; + + int m_nextDisguise; + + bool m_isWaitingForFullReload; + + void FireWeaponAtEnemy( CTFBot *me ); + + CHandle< CBaseEntity > m_lastTouch; + float m_lastTouchTime; + + bool IsImmediateThreat( const CBaseCombatCharacter *subject, const CKnownEntity *threat ) const; + const CKnownEntity *SelectCloserThreat( CTFBot *me, const CKnownEntity *threat1, const CKnownEntity *threat2 ) const; + const CKnownEntity *GetHealerOfThreat( const CKnownEntity *threat ) const; + + const CKnownEntity *SelectMoreDangerousThreatInternal( const INextBot *me, + const CBaseCombatCharacter *subject, + const CKnownEntity *threat1, + const CKnownEntity *threat2 ) const; + + + void Dodge( CTFBot *me ); + + IntervalTimer m_undergroundTimer; +}; + + + +#endif // TF_BOT_BEHAVIOR_H diff --git a/game/server/tf/bot/behavior/tf_bot_dead.cpp b/game/server/tf/bot/behavior/tf_bot_dead.cpp new file mode 100644 index 0000000..6e94d99 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_dead.cpp @@ -0,0 +1,58 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_dead.cpp +// Push up daisies +// Michael Booth, May 2009 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_dead.h" +#include "bot/behavior/tf_bot_behavior.h" + +#include "nav_mesh.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDead::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_deadTimer.Start(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDead::Update( CTFBot *me, float interval ) +{ + if ( me->IsAlive() ) + { + // how did this happen? + return ChangeTo( new CTFBotMainAction, "This should not happen!" ); + } + + if ( m_deadTimer.IsGreaterThen( 5.0f ) ) + { + if ( me->HasAttribute( CTFBot::REMOVE_ON_DEATH ) ) + { + // remove dead bots + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", me->GetUserID() ) ); + } + else if ( me->HasAttribute( CTFBot::BECOME_SPECTATOR_ON_DEATH ) ) + { + me->ChangeTeam( TEAM_SPECTATOR, false, true ); + return Done(); + } + } + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() && me->GetTeamNumber() == TF_TEAM_RED ) + { + // dead defenders go to spectator for recycling + me->ChangeTeam( TEAM_SPECTATOR, false, true ); + } +#endif // TF_RAID_MODE + + return Continue(); +} + diff --git a/game/server/tf/bot/behavior/tf_bot_dead.h b/game/server/tf/bot/behavior/tf_bot_dead.h new file mode 100644 index 0000000..796db43 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_dead.h @@ -0,0 +1,23 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_dead.h +// Push up daisies +// Michael Booth, May 2009 + +#ifndef TF_BOT_DEAD_H +#define TF_BOT_DEAD_H + +#include "Path/NextBotChasePath.h" + +class CTFBotDead : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "Dead"; }; + +private: + IntervalTimer m_deadTimer; +}; + +#endif // TF_BOT_DEAD_H diff --git a/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.cpp b/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.cpp new file mode 100644 index 0000000..e539436 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.cpp @@ -0,0 +1,959 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_destroy_enemy_sentry.cpp +// Destroy an enemy sentry gun +// Michael Booth, June 2010 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_obj_sentrygun.h" +#include "tf_weaponbase_gun.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_destroy_enemy_sentry.h" +#include "bot/behavior/tf_bot_attack.h" +#include "bot/behavior/tf_bot_retreat_to_cover.h" +#include "bot/behavior/tf_bot_get_ammo.h" +#include "bot/behavior/demoman/tf_bot_stickybomb_sentrygun.h" + +#include "nav_mesh.h" + + +extern ConVar tf_bot_path_lookahead_range; +extern ConVar tf_bot_sticky_base_range; + +ConVar tf_bot_debug_destroy_enemy_sentry( "tf_bot_debug_destroy_enemy_sentry", "0", FCVAR_CHEAT ); +ConVar tf_bot_max_grenade_launch_at_sentry_range( "tf_bot_max_grenade_launch_at_sentry_range", "1500", FCVAR_CHEAT ); +ConVar tf_bot_max_sticky_launch_at_sentry_range( "tf_bot_max_sticky_launch_at_sentry_range", "1500", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +// Search for angle to land grenade near target +bool FindGrenadeAim( CTFBot *me, CBaseEntity *target, float *aimYaw, float *aimPitch ) +{ + Vector toTarget = target->WorldSpaceCenter() - me->EyePosition(); + + if ( toTarget.IsLengthGreaterThan( tf_bot_max_grenade_launch_at_sentry_range.GetFloat() ) ) + { + return false; + } + + QAngle anglesToTarget; + VectorAngles( toTarget, anglesToTarget ); + + // start with current aim, in case we're already on target + const QAngle &eyeAngles = me->EyeAngles(); + float yaw = eyeAngles.y; + float pitch = eyeAngles.x; + + const int trials = 10; + for( int t=0; t<trials; ++t ) + { + // estimate impact spot + const float pipebombInitVel = 900.0f; + Vector impactSpot = me->EstimateProjectileImpactPosition( pitch, yaw, pipebombInitVel ); + + // check if impactSpot landed near sentry + const float explosionRadius = 75.0f; + if ( ( target->WorldSpaceCenter() - impactSpot ).IsLengthLessThan( explosionRadius ) ) + { + trace_t trace; + NextBotTraceFilterIgnoreActors filter( target, COLLISION_GROUP_NONE ); + + UTIL_TraceLine( target->WorldSpaceCenter(), impactSpot, MASK_SOLID_BRUSHONLY, &filter, &trace ); + if ( !trace.DidHit() ) + { + *aimYaw = yaw; + *aimPitch = pitch; + return true; + } + } + + yaw = anglesToTarget.y + RandomFloat( -30.0f, 30.0f ); + pitch = RandomFloat( -85.0f, 85.0f ); + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +// Search for angle to land sticky near target +bool FindStickybombAim( CTFBot *me, CBaseEntity *target, float *aimYaw, float *aimPitch, float *aimCharge ) +{ + Vector toTarget = target->WorldSpaceCenter() - me->EyePosition(); + + if ( toTarget.IsLengthGreaterThan( tf_bot_max_sticky_launch_at_sentry_range.GetFloat() ) ) + { + return false; + } + + QAngle anglesToTarget; + VectorAngles( toTarget, anglesToTarget ); + + // start with current aim, in case we're already on target + const QAngle &eyeAngles = me->EyeAngles(); + + float yaw = eyeAngles.y; + float pitch = eyeAngles.x; + + *aimCharge = 1.0f; + + bool hasTarget = false; + + const int trials = 100; + for( int t=0; t<trials; ++t ) + { + float charge = 0.0f; +// if ( toTarget.IsLengthGreaterThan( tf_bot_sticky_base_range.GetBool() ) ) +// { +// charge = RandomFloat( 0.1f, 1.0f ); +// +// // skew towards zero - full charge shots are seldom required +// charge *= charge; +// } + + // estimate impact spot + Vector impactSpot = me->EstimateStickybombProjectileImpactPosition( pitch, yaw, charge ); + + // check if impactSpot landed near target + const float explosionRadius = 75.0f; + if ( ( target->WorldSpaceCenter() - impactSpot ).IsLengthLessThan( explosionRadius ) ) + { + trace_t trace; + NextBotTraceFilterIgnoreActors filter( target, COLLISION_GROUP_NONE ); + + UTIL_TraceLine( target->WorldSpaceCenter(), impactSpot, MASK_SOLID_BRUSHONLY, &filter, &trace ); + if ( !trace.DidHit() ) + { + // found target aim - keep one we find with least required + // charge, because we need to be fast in combat + if ( charge < (*aimCharge) ) + { + hasTarget = true; + + *aimCharge = charge; + *aimYaw = yaw; + *aimPitch = pitch; + + if ( *aimCharge < 0.01 ) + { + // as quick as possible - no need to search further + break; + } + } + } + } + + yaw = anglesToTarget.y + RandomFloat( -30.0f, 30.0f ); + pitch = RandomFloat( -85.0f, 85.0f ); + } + + return hasTarget; +} + + + + + +//--------------------------------------------------------------------------------------------- +// Return true if this Action has what it needs to perform right now +bool CTFBotDestroyEnemySentry::IsPossible( CTFBot *me ) +{ + if ( me->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) || + me->IsPlayerClass( TF_CLASS_SNIPER ) || + me->IsPlayerClass( TF_CLASS_MEDIC ) || + me->IsPlayerClass( TF_CLASS_ENGINEER ) || + me->IsPlayerClass( TF_CLASS_PYRO ) ) + { + // these classes have no way to kill a sentry at long range + return false; + } + + // don't go after a sentry if we're out of ammo + if ( me->GetAmmoCount( TF_AMMO_PRIMARY ) <= 0 || me->GetAmmoCount( TF_AMMO_SECONDARY ) <= 0 ) + { + return false; + } + + // if we're a spy, we have better ways of destroying sentries that shooting at it + if ( me->IsPlayerClass( TF_CLASS_SPY ) ) + { + return false; + } + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() ) + { + if ( me->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + return false; + } + } +#endif + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + if ( me->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + return false; + } + } + + return true; +} + + +//--------------------------------------------------------------------------------------------- +class CFindSafeAttackArea : public ISearchSurroundingAreasFunctor +{ +public: + CFindSafeAttackArea( CTFBot *me ) + { + m_me = me; + m_attackSpot = me->GetAbsOrigin(); + m_foundAttackSpot = false; + + CObjectSentrygun *sentry = me->GetEnemySentry(); + if ( sentry ) + { + sentry->UpdateLastKnownArea(); + m_sentryArea = (CTFNavArea *)sentry->GetLastKnownArea(); + } + else + { + m_sentryArea = NULL; + } + } + + virtual bool operator() ( CNavArea *area, CNavArea *priorArea, float travelDistanceSoFar ) + { + if ( !m_sentryArea ) + { + return false; + } + + if ( area->IsPotentiallyVisible( m_sentryArea ) ) + { + // try the center first + m_attackSpot = area->GetCenter(); + + const int maxTries = 5; + for( int i=0; i<maxTries; ++i ) + { + if ( m_me->IsLineOfFireClear( m_attackSpot + m_me->GetClassEyeHeight(), m_me->GetEnemySentry() ) ) + { + if ( ( m_attackSpot - m_me->GetEnemySentry()->GetAbsOrigin() ).IsLengthGreaterThan( 1.1f * SENTRY_MAX_RANGE ) ) + { + // found our attack spot + m_foundAttackSpot = true; + return false; + } + } + + m_attackSpot = area->GetRandomPoint(); + } + } + + return true; + } + + + CTFBot *m_me; + CTFNavArea *m_sentryArea; + Vector m_attackSpot; + bool m_foundAttackSpot; + + Vector m_splashFromSpot; + Vector m_splashToSpot; + bool m_foundSplashSpot; +}; + + +//--------------------------------------------------------------------------------------------- +void CTFBotDestroyEnemySentry::ComputeSafeAttackSpot( CTFBot *me ) +{ + m_hasSafeAttackSpot = false; + + CObjectSentrygun *sentry = me->GetEnemySentry(); + if ( sentry == NULL ) + { + return; + } + + sentry->UpdateLastKnownArea(); + + CTFNavArea *sentryArea = (CTFNavArea *)sentry->GetLastKnownArea(); + if ( sentryArea == NULL ) + { + return; + } + + NavAreaCollector collector( true ); + sentryArea->ForAllPotentiallyVisibleAreas( collector ); + + int i; + CUtlVector< CTFNavArea * > beyondSentryRangeVector; + for( i=0; i<collector.m_area.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)collector.m_area[i]; + + Vector wayOut = ( area->GetCenter() - sentryArea->GetCenter() ) + area->GetCenter(); + + Vector farthestFromSentry; + area->GetClosestPointOnArea( wayOut, &farthestFromSentry ); + + if ( ( farthestFromSentry - sentry->GetAbsOrigin() ).IsLengthGreaterThan( SENTRY_MAX_RANGE ) ) + { + // at least some of this area is out of sentry range + beyondSentryRangeVector.AddToTail( area ); + + if ( tf_bot_debug_destroy_enemy_sentry.GetBool() ) + { + area->DrawFilled( 0, 255, 0, 255, 60.0f, true, 1.0f ); + } + } + } + + + CUtlVector< CTFNavArea * > attackSentryVector; + for( i=0; i<beyondSentryRangeVector.Count(); ++i ) + { + CTFNavArea *area = beyondSentryRangeVector[i]; + + Vector closestToSentry; + area->GetClosestPointOnArea( sentry->GetAbsOrigin(), &closestToSentry ); + + if ( ( closestToSentry - sentry->GetAbsOrigin() ).IsLengthLessThan( 1.5f * SENTRY_MAX_RANGE ) ) + { + // good attack range + attackSentryVector.AddToTail( area ); + + if ( tf_bot_debug_destroy_enemy_sentry.GetBool() ) + { + area->DrawFilled( 100, 255, 0, 255, 60.0f ); + } + } + } + + + if ( beyondSentryRangeVector.Count() == 0 ) + { + // no safe areas at all + m_hasSafeAttackSpot = false; + return; + } + + CUtlVector< CTFNavArea * > *safeAreaVector; + + if ( attackSentryVector.Count() == 0 ) + { + // no good close-in attack areas, choose from farther away set + safeAreaVector = &beyondSentryRangeVector; + } + else + { + // for now, just pick a random spot + safeAreaVector = &attackSentryVector; + } + + // TODO: find closest and least combat-hot area + CTFNavArea *safeArea = safeAreaVector->Element( RandomInt( 0, safeAreaVector->Count()-1 ) ); + + m_safeAttackSpot = safeArea->GetRandomPoint(); + m_hasSafeAttackSpot = true; + + if ( tf_bot_debug_destroy_enemy_sentry.GetBool() ) + { + safeArea->DrawFilled( 255, 255, 0, 255, 60.0f ); + NDebugOverlay::Cross3D( m_safeAttackSpot, 10.0f, 255, 0, 0, true, 60.0f ); + } +} + + +//--------------------------------------------------------------------------------------------- +class FindSafeSentryApproachAreaScan : public ISearchSurroundingAreasFunctor +{ +public: + FindSafeSentryApproachAreaScan( CTFBot *me ) + { + m_me = me; + + m_isEscaping = false; + + CTFNavArea *myArea = me->GetLastKnownArea(); + if ( myArea && myArea->IsTFMarked() ) + { + // I'm standing in a danger area - escape! + m_isEscaping = true; + } + } + + virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) + { + CTFNavArea *area = (CTFNavArea *)baseArea; + + if ( m_isEscaping ) + { + if ( !area->IsTFMarked() ) + { + // found safe area - use it + m_approachAreaVector.AddToTail( area ); + return false; + } + } + else + { + if ( area->IsTFMarked() && priorArea ) + { + // we just stepped into sentry fire - keep the area one step prior + m_approachAreaVector.AddToTail( (CTFNavArea *)priorArea ); + } + } + + return true; + } + + // return true if 'adjArea' should be included in the ongoing search + virtual bool ShouldSearch( CNavArea *baseAdjArea, CNavArea *baseCurrentArea, float travelDistanceSoFar ) + { + CTFNavArea *area = (CTFNavArea *)baseCurrentArea; + + if ( !m_isEscaping ) + { + // don't search beyond sentry danger areas (but step into them) + if ( area->IsTFMarked() ) + { + return false; + } + } + + return m_me->GetLocomotionInterface()->IsAreaTraversable( baseAdjArea ); + } + + // Invoked after the search has completed + virtual void PostSearch( void ) + { + if ( tf_bot_debug_destroy_enemy_sentry.GetBool() ) + { + for( int i=0; i<m_approachAreaVector.Count(); ++i ) + { + m_approachAreaVector[i]->DrawFilled( 0, 255, 0, 255, 60.0f ); + } + } + } + + CTFBot *m_me; + CUtlVector< CTFNavArea * > m_approachAreaVector; + bool m_isEscaping; +}; + + +//--------------------------------------------------------------------------------------------- +void CTFBotDestroyEnemySentry::ComputeCornerAttackSpot( CTFBot *me ) +{ + m_safeAttackSpot = vec3_origin; + m_hasSafeAttackSpot = false; + + CObjectSentrygun *sentry = me->GetEnemySentry(); + if ( !sentry ) + { + return; + } + + sentry->UpdateLastKnownArea(); + CTFNavArea *sentryArea = (CTFNavArea *)sentry->GetLastKnownArea(); + + if ( !sentryArea ) + { + return; + } + + // mark all areas this sentry can potentially fire upon + // need to use completely visible so the partially visible areas are used as corner-fighting spots + NavAreaCollector sentryDanger; + sentryArea->ForAllCompletelyVisibleAreas( sentryDanger ); + + CTFNavArea::MakeNewTFMarker(); + for( int i=0; i<sentryDanger.m_area.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)sentryDanger.m_area[i]; + + Vector close; + area->GetClosestPointOnArea( sentry->GetAbsOrigin(), &close ); + + if ( ( sentry->GetAbsOrigin() - close ).IsLengthLessThan( SENTRY_MAX_RANGE ) ) + { + area->TFMark(); + + if ( tf_bot_debug_destroy_enemy_sentry.GetBool() ) + { + area->DrawFilled( 255, 0, 0, 255, 60.0f ); + } + } + } + + + // find nearby area adjacent to area that is in enemy sentry fire field + FindSafeSentryApproachAreaScan scan( me ); + SearchSurroundingAreas( me->GetLastKnownArea(), scan ); + + if ( scan.m_approachAreaVector.Count() > 0 ) + { + CTFNavArea *safeArea = scan.m_approachAreaVector[ RandomInt( 0, scan.m_approachAreaVector.Count()-1 ) ]; + + // try to avoid picking a spot where sentry can attack us + const int retryCount = 25; + for( int r=0; r<retryCount; ++r ) + { + m_safeAttackSpot = safeArea->GetRandomPoint(); + + if ( ( sentry->WorldSpaceCenter() - m_safeAttackSpot ).IsLengthGreaterThan( SENTRY_MAX_RANGE ) || + !me->IsLineOfFireClear( sentry->WorldSpaceCenter(), m_safeAttackSpot ) ) + { + break; + } + } + + m_hasSafeAttackSpot = true; + + if ( tf_bot_debug_destroy_enemy_sentry.GetBool() ) + { + NDebugOverlay::Cross3D( m_safeAttackSpot, 5.0f, 255, 255, 0, true, 60.0f ); + } + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDestroyEnemySentry::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + m_path.Invalidate(); + m_repathTimer.Invalidate(); + + m_isAttackingSentry = false; + m_wasUber = false; + +/* + // find a spot to attack the sentry out of its range + CFindSafeAttackArea find( me ); + SearchSurroundingAreas( me->GetLastKnownArea(), find, 1.5f * SENTRY_MAX_RANGE ); + + m_hasSafeAttackSpot = find.m_foundAttackSpot; + m_safeAttackSpot = find.m_attackSpot; +*/ + + if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + ComputeCornerAttackSpot( me ); + } + else + { + ComputeSafeAttackSpot( me ); + } + +/* + if ( !m_hasSafeAttackSpot ) + { + return Done( "No safe attack spot found" ); + } +*/ + + m_targetSentry = me->GetEnemySentry(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDestroyEnemySentry::Update( CTFBot *me, float interval ) +{ + if ( me->GetEnemySentry() == NULL ) + { + return Done( "Enemy sentry is destroyed" ); + } + + // if the sentry changes, re-evaluate + if ( me->GetEnemySentry() != m_targetSentry ) + { + return ChangeTo( new CTFBotDestroyEnemySentry, "Changed sentry target" ); + } + + if ( me->m_Shared.IsInvulnerable() ) + { + if ( !m_wasUber ) + { + m_wasUber = true; + + // we just became uber - are we close enough to rush the sentry? + const float maxRushDistance = 500.0f; + CTFBotPathCost cost( me, FASTEST_ROUTE ); + float travelDistance = NavAreaTravelDistance( me->GetLastKnownArea(), + m_targetSentry->GetLastKnownArea(), + cost, maxRushDistance ); + + if ( travelDistance >= 0.0f ) + { + return SuspendFor( new CTFBotUberAttackEnemySentry( m_targetSentry ), "Go get it!" ); + } + } + } + else + { + m_wasUber = false; + } + + if ( !me->HasAttribute( CTFBot::IGNORE_ENEMIES ) ) + { + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleInFOVNow() ) + { + float threatRange = me->GetRangeTo( threat->GetLastKnownPosition() ); + float sentryRange = me->GetRangeTo( me->GetEnemySentry() ); + + if ( threatRange < 0.5f * sentryRange ) + { + return Done( "Enemy near" ); + } + } + } + + bool isSentryFiringOnMe = false; + if ( me->GetEnemySentry()->GetTimeSinceLastFired() < 1.0f ) + { + Vector sentryForward; + AngleVectors( me->GetEnemySentry()->GetTurretAngles(), &sentryForward ); + + Vector to = me->GetAbsOrigin() - me->GetEnemySentry()->GetAbsOrigin(); + to.NormalizeInPlace(); + + if ( DotProduct( to, sentryForward ) > 0.8f ) + { + isSentryFiringOnMe = true; + } + } + + + if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + // a demoman wants to get close to the sentry but just out of range or line of sight so + // he can pepper the area with stickies and destroy it + Vector attackSpot = m_hasSafeAttackSpot ? m_safeAttackSpot : m_targetSentry->GetAbsOrigin(); + + // move into position + if ( !m_path.IsValid() || m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( 1.0f ); + + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_path.Compute( me, attackSpot, cost ); + } + + float aimPitch, aimYaw, aimCharge; + if ( isSentryFiringOnMe ) + { + // the sentry is firing on me - might as well shoot back! + me->EquipLongRangeWeapon(); + me->PressFireButton(); + } + else if ( FindStickybombAim( me, m_targetSentry, &aimYaw, &aimPitch, &aimCharge ) ) + { + // found an opportunistic spot to sticky the sentry from + return ChangeTo( new CTFBotStickybombSentrygun( me->GetEnemySentry(), aimYaw, aimPitch, aimCharge ), "Destroying sentry with opportunistic sticky shot" ); + } + + // move towards sentry + if ( m_canMove ) + { + m_path.Update( me ); + } + + if ( ( me->IsRangeLessThan( attackSpot, 50.0f ) && + ( me->GetAbsOrigin() - attackSpot ).AsVector2D().IsLengthLessThan( 25.0f ) ) || + ( me->IsLineOfFireClear( me->GetEnemySentry() ) && me->IsRangeLessThan( m_targetSentry, 1000.0f ) ) ) // opportunistic shot + { + // reached attack spot + return ChangeTo( new CTFBotStickybombSentrygun( me->GetEnemySentry() ), "Destroying sentry with stickies" ); + } + + if ( me->IsRangeLessThan( attackSpot, 200.0f ) ) + { +#ifdef TF_CREEP_MODE + if ( m_creepTimer.IsElapsed() ) + { + m_canMove = !m_canMove; + + if ( m_canMove ) + { + m_creepTimer.Start( 0.1f ); + } + else + { + m_creepTimer.Start( RandomFloat( 0.2f, 0.5f ) ); + } + } +#endif + } + else + { + m_canMove = true; + } + + return Continue(); + } + + + bool isInAttackPosition = ( m_hasSafeAttackSpot && me->IsRangeLessThan( m_safeAttackSpot, 20.0f ) ); + + if ( isInAttackPosition || me->IsLineOfFireClear( me->GetEnemySentry() ) ) + { + // must look at sentry entity to make use of SelectTargetPoint() + me->GetBodyInterface()->AimHeadTowards( me->GetEnemySentry(), IBody::MANDATORY, 1.0f, NULL, "Aiming at enemy sentry" ); + + // because sentries are stationary, check if XY is on target to allow SelectTargetPoint() to adjust Z for grenades + Vector toSentry = me->GetEnemySentry()->WorldSpaceCenter() - me->EyePosition(); + toSentry.NormalizeInPlace(); + Vector forward; + me->EyeVectors( &forward ); + + if ( ( forward.x * toSentry.x + forward.y * toSentry.y ) > 0.95f ) + { + if ( me->EquipLongRangeWeapon() == false ) + { + return SuspendFor( new CTFBotRetreatToCover( 0.1f ), "No suitable range weapon available right now" ); + } + + me->PressFireButton(); + m_isAttackingSentry = true; + } + else + { + m_isAttackingSentry = false; + } + + if ( me->IsRangeGreaterThan( me->GetEnemySentry(), 1.1f * SENTRY_MAX_RANGE ) ) + { + // safely out of range of the gun - hold here and fire at it + return Continue(); + } + + // we are in range of the gun - if it is pointed at us and firing, retreat to cover + if ( me->GetEnemySentry()->GetTimeSinceLastFired() < 1.0f ) + { + Vector sentryForward; + AngleVectors( me->GetEnemySentry()->GetTurretAngles(), &sentryForward ); + + Vector to = me->GetAbsOrigin() - me->GetEnemySentry()->GetAbsOrigin(); + to.NormalizeInPlace(); + + if ( DotProduct( to, sentryForward ) > 0.8f ) + { + return SuspendFor( new CTFBotRetreatToCover( 0.1f ), "Taking cover from sentry fire" ); + } + } + + if ( isInAttackPosition ) + { + // we're at our attack position, hold here + return Continue(); + } + } + + // move into position + if ( !m_path.IsValid() || m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( 1.0f ); + + CTFBotPathCost cost( me, SAFEST_ROUTE ); + Vector moveGoal = m_hasSafeAttackSpot ? m_safeAttackSpot : me->GetEnemySentry()->GetAbsOrigin(); + + if ( !m_path.Compute( me, moveGoal, cost ) ) + { + return Done( "No path" ); + } + } + + // move along path to vantage point + m_path.Update( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotDestroyEnemySentry::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + m_path.Invalidate(); + m_repathTimer.Invalidate(); + + if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + ComputeCornerAttackSpot( me ); + } + else + { + ComputeSafeAttackSpot( me ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotDestroyEnemySentry::ShouldHurry( const INextBot *me ) const +{ + // while killing a sentry we're "hurrying" so we don't dodge + return m_isAttackingSentry ? ANSWER_YES : ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotDestroyEnemySentry::ShouldRetreat( const INextBot *me ) const +{ + // push in to kill the sentry + return ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotDestroyEnemySentry::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + // if we're in range to attack the sentry, we handle firing directly + return m_isAttackingSentry ? ANSWER_NO : ANSWER_UNDEFINED; +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CTFBotUberAttackEnemySentry::CTFBotUberAttackEnemySentry( CObjectSentrygun *sentryTarget ) +{ + m_targetSentry = sentryTarget; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotUberAttackEnemySentry::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_wasIgnoringEnemies = me->HasAttribute( CTFBot::IGNORE_ENEMIES ); + + me->SetAttribute( CTFBot::IGNORE_ENEMIES ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotUberAttackEnemySentry::Update( CTFBot *me, float interval ) +{ + if ( !me->m_Shared.InCond( TF_COND_INVULNERABLE ) ) + { + return Done( "No longer uber" ); + } + + if ( m_targetSentry == NULL ) + { + return Done( "Target sentry destroyed" ); + } + + float aimYaw, aimPitch; + if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) && FindGrenadeAim( me, m_targetSentry, &aimYaw, &aimPitch ) ) + { + QAngle aimAngles; + aimAngles.x = aimPitch; + aimAngles.y = aimYaw; + aimAngles.z = 0.0f; + + Vector aimForward; + AngleVectors( aimAngles, &aimForward ); + + // always recompute eye aim target so we can update our view + Vector eyeAimTarget = me->EyePosition() + 5000.0f * aimForward; + me->GetBodyInterface()->AimHeadTowards( eyeAimTarget, IBody::CRITICAL, 0.3f, NULL, "Aiming at opportunistic grenade shot" ); + + Vector eyeForward; + me->EyeVectors( &eyeForward ); + + if ( DotProduct( aimForward, eyeForward ) > 0.9f ) + { + if ( me->EquipLongRangeWeapon() == false ) + { + return SuspendFor( new CTFBotRetreatToCover( 0.1f ), "No suitable range weapon available right now" ); + } + + me->PressFireButton(); + } + } + else if ( me->IsLineOfFireClear( m_targetSentry ) ) + { + // must look at sentry entity to make use of SelectTargetPoint() + me->GetBodyInterface()->AimHeadTowards( m_targetSentry, IBody::MANDATORY, 1.0f, NULL, "Aiming at target sentry" ); + + // because sentries are stationary, check if XY is on target to allow SelectTargetPoint() to adjust Z for grenades + Vector toSentry = m_targetSentry->WorldSpaceCenter() - me->EyePosition(); + toSentry.NormalizeInPlace(); + + Vector eyeForward; + me->EyeVectors( &eyeForward ); + + if ( ( eyeForward.x * toSentry.x + eyeForward.y * toSentry.y ) > 0.95f ) + { + if ( me->EquipLongRangeWeapon() == false ) + { + return SuspendFor( new CTFBotRetreatToCover( 0.1f ), "No suitable range weapon available right now" ); + } + + me->PressFireButton(); + } + + if ( me->IsRangeLessThan( m_targetSentry, 100.0f ) ) + { + // we have a clear line of fire and are close enough + return Continue(); + } + } + + // move into position + if ( !m_path.IsValid() || m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( 1.0f ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, m_targetSentry->WorldSpaceCenter(), cost ); + } + + m_path.Update( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotUberAttackEnemySentry::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + if ( !m_wasIgnoringEnemies ) + { + me->ClearAttribute( CTFBot::IGNORE_ENEMIES ); + } +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotUberAttackEnemySentry::ShouldHurry( const INextBot *me ) const +{ + return ANSWER_YES; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotUberAttackEnemySentry::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotUberAttackEnemySentry::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_YES; +} diff --git a/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.h b/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.h new file mode 100644 index 0000000..f12d31a --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_destroy_enemy_sentry.h @@ -0,0 +1,79 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_destroy_enemy_sentry.h +// Destroy an enemy sentry gun +// Michael Booth, June 2010 + +#ifndef TF_BOT_DESTROY_ENEMY_SENTRY_H +#define TF_BOT_DESTROY_ENEMY_SENTRY_H + +#include "Path/NextBotChasePath.h" + +//--------------------------------------------------------------------------------- +class CTFBotDestroyEnemySentry : public Action< CTFBot > +{ +public: + static bool IsPossible( CTFBot *me ); // return true if this Action has what it needs to perform right now + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "DestroyEnemySentry"; }; + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; + + bool m_canMove; + +#ifdef TF_CREEP_MODE + CountdownTimer m_creepTimer; +#endif + + Vector m_safeAttackSpot; + bool m_hasSafeAttackSpot; + void ComputeSafeAttackSpot( CTFBot *me ); + void ComputeCornerAttackSpot( CTFBot *me ); + + bool m_isAttackingSentry; + bool m_wasUber; + + ActionResult< CTFBot > EquipLongRangeWeapon( CTFBot *me ); + + CHandle< CObjectSentrygun > m_targetSentry; +}; + + +//--------------------------------------------------------------------------------- +class CTFBotUberAttackEnemySentry : public Action< CTFBot > +{ +public: + CTFBotUberAttackEnemySentry( CObjectSentrygun *sentryTarget ); + virtual ~CTFBotUberAttackEnemySentry() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + + virtual const char *GetName( void ) const { return "UberAttackEnemySentry"; }; + +private: + bool m_wasIgnoringEnemies; + + PathFollower m_path; + CountdownTimer m_repathTimer; + + CHandle< CObjectSentrygun > m_targetSentry; +}; + + +#endif // TF_BOT_DESTROY_ENEMY_SENTRY_H diff --git a/game/server/tf/bot/behavior/tf_bot_escort.cpp b/game/server/tf/bot/behavior/tf_bot_escort.cpp new file mode 100644 index 0000000..08a2f77 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_escort.cpp @@ -0,0 +1,130 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_escort.cpp +// Move near an entity and protect it +// Michael Booth, April 2011 + +#include "cbase.h" + +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_escort.h" +#include "bot/behavior/tf_bot_attack.h" +#include "bot/behavior/demoman/tf_bot_prepare_stickybomb_trap.h" +#include "bot/behavior/tf_bot_destroy_enemy_sentry.h" + +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_escort_range( "tf_bot_escort_range", "300", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +CTFBotEscort::CTFBotEscort( CBaseEntity *who ) +{ + SetWho( who ); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotEscort::SetWho( CBaseEntity *who ) +{ + m_who = who; +} + + +//--------------------------------------------------------------------------------------------- +CBaseEntity *CTFBotEscort::GetWho( void ) const +{ + return m_who; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEscort::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_pathToWho.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotEscort::Update( CTFBot *me, float interval ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleInFOVNow() ) + { + return SuspendFor( new CTFBotAttack, "Attacking nearby threat" ); + } + else + { + // no enemy is visible - move near who we are escorting + if ( m_who != NULL ) + { + if ( me->IsRangeGreaterThan( m_who, tf_bot_escort_range.GetFloat() ) ) + { + if ( m_repathTimer.IsElapsed() ) + { + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_pathToWho.Compute( me, m_who->GetAbsOrigin(), cost ); + m_repathTimer.Start( RandomFloat( 2.0f, 3.0f ) ); + } + + m_pathToWho.Update( me ); + } + else + { + if ( CTFBotPrepareStickybombTrap::IsPossible( me ) ) + { + return SuspendFor( new CTFBotPrepareStickybombTrap, "Laying sticky bombs!" ); + } + } + } + + // destroy enemy sentry guns we've encountered + if ( me->GetEnemySentry() && CTFBotDestroyEnemySentry::IsPossible( me ) ) + { + return SuspendFor( new CTFBotDestroyEnemySentry, "Going after an enemy sentry to destroy it" ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEscort::OnStuck( CTFBot *me ) +{ + m_repathTimer.Invalidate(); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEscort::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEscort::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotEscort::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_NO; +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotEscort::OnCommandApproach( CTFBot *me, const Vector &pos, float range ) +{ + return TryContinue(); +} diff --git a/game/server/tf/bot/behavior/tf_bot_escort.h b/game/server/tf/bot/behavior/tf_bot_escort.h new file mode 100644 index 0000000..53e4cc0 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_escort.h @@ -0,0 +1,40 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_escort.cpp +// Move near an entity and protect it +// Michael Booth, April 2011 + +#ifndef TF_BOT_ESCORT_H +#define TF_BOT_ESCORT_H + +#include "Path/NextBotChasePath.h" + +class CTFBotEscort : public Action< CTFBot > +{ +public: + CTFBotEscort( CBaseEntity *who ); + virtual ~CTFBotEscort() { } + + void SetWho( CBaseEntity *who ); + CBaseEntity *GetWho( void ) const; + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + + virtual EventDesiredResult< CTFBot > OnCommandApproach( CTFBot *me, const Vector &pos, float range ); + + virtual const char *GetName( void ) const { return "Escort"; } + +private: + CHandle< CBaseEntity > m_who; + PathFollower m_pathToWho; + CountdownTimer m_vocalizeTimer; + CountdownTimer m_repathTimer; +}; + +#endif // TF_BOT_ESCORT_H diff --git a/game/server/tf/bot/behavior/tf_bot_get_ammo.cpp b/game/server/tf/bot/behavior/tf_bot_get_ammo.cpp new file mode 100644 index 0000000..302663f --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_get_ammo.cpp @@ -0,0 +1,339 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_get_ammo.h +// Pick up any nearby ammo +// Michael Booth, May 2009 + +#include "cbase.h" +#include "tf_obj.h" +#include "tf_gamerules.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_get_ammo.h" + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_ammo_search_range( "tf_bot_ammo_search_range", "5000", FCVAR_CHEAT, "How far bots will search to find ammo around them" ); +ConVar tf_bot_debug_ammo_scavenging( "tf_bot_debug_ammo_scavenging", "0", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +CTFBotGetAmmo::CTFBotGetAmmo( void ) +{ + m_path.Invalidate(); + m_ammo = NULL; + m_isGoalDispenser = false; +} + + +//--------------------------------------------------------------------------------------------- +class CAmmoFilter : public INextBotFilter +{ +public: + CAmmoFilter( CTFBot *me ) + { + m_me = me; + m_ammoArea = NULL; + } + + bool IsSelected( const CBaseEntity *constCandidate ) const + { + CBaseEntity *candidate = const_cast< CBaseEntity * >( constCandidate ); + + m_ammoArea = (CTFNavArea *)TheNavMesh->GetNearestNavArea( candidate->WorldSpaceCenter() ); + if ( !m_ammoArea ) + return false; + + CClosestTFPlayer close( candidate ); + ForEachPlayer( close ); + + // if the closest player to this candidate object is an enemy, don't use it + if ( close.m_closePlayer && !m_me->InSameTeam( close.m_closePlayer ) ) + return false; + + // resupply cabinets (not assigned a team) + if ( candidate->ClassMatches( "func_regenerate" ) ) + { + if ( !m_ammoArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) ) + { + // Assume any resupply cabinets not in a teamed spawn room are inaccessible. + // Ex: pl_upward has forward spawn rooms that neither team can use until + // certain checkpoints are reached. + return false; + } + + if ( ( m_me->GetTeamNumber() == TF_TEAM_RED && m_ammoArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) ) || + ( m_me->GetTeamNumber() == TF_TEAM_BLUE && m_ammoArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) ) + { + // the supply cabinet is in my spawn room, or not in any spawn room + return true; + } + return false; + } + + // ignore non-existent ammo to ensure we collect nearby existing ammo + if ( candidate->IsEffectActive( EF_NODRAW ) ) + return false; + + if ( candidate->ClassMatches( "tf_ammo_pack" ) ) + return true; + + if ( candidate->ClassMatches( "item_ammopack*" ) ) + return true; + + if ( m_me->InSameTeam( candidate ) ) + { + // friendly engineer's dispenser + if ( candidate->ClassMatches( "obj_dispenser*" ) ) + { + // for now, assume Engineers want to go fetch ammo boxes unless their dispenser is fully upgraded + // unless we have no sentry yet, then we need to leech off of buddy's dispenser to get started + if ( !m_me->IsPlayerClass( TF_CLASS_ENGINEER ) || ( (CBaseObject *)candidate )->GetUpgradeLevel() >= 3 || !m_me->GetObjectOfType( OBJ_SENTRYGUN ) ) + { + CBaseObject *dispenser = (CBaseObject *)candidate; + if ( !dispenser->IsBuilding() && !dispenser->IsDisabled() ) + { + return true; + } + } + } + } + + return false; + } + + CTFBot *m_me; + mutable CTFNavArea *m_ammoArea; +}; + + +//--------------------------------------------------------------------------------------------- +static CTFBot *s_possibleBot = NULL; +static CHandle< CBaseEntity > s_possibleAmmo = NULL; +static int s_possibleFrame = 0; + + +//--------------------------------------------------------------------------------------------- +/** + * Return true if this Action has what it needs to perform right now + */ +bool CTFBotGetAmmo::IsPossible( CTFBot *me ) +{ + VPROF_BUDGET( "CTFBotGetAmmo::IsPossible", "NextBot" ); + + int i; + + CUtlVector< CNavArea * > nearbyAreaVector; + CollectSurroundingAreas( &nearbyAreaVector, me->GetLastKnownArea(), tf_bot_ammo_search_range.GetFloat(), me->GetLocomotionInterface()->GetStepHeight(), me->GetLocomotionInterface()->GetDeathDropHeight() ); + + CAmmoFilter ammoFilter( me ); + + const CUtlVector< CHandle< CBaseEntity > > &staticAmmoVector = TFGameRules()->GetAmmoEntityVector(); + CBaseEntity *closestAmmo = NULL; + float closestAmmoTravelDistance = FLT_MAX; + + for( i=0; i<staticAmmoVector.Count(); ++i ) + { + CBaseEntity *ammo = staticAmmoVector[i]; + if ( ammo ) + { + if ( ammoFilter.IsSelected( ammo ) ) + { + if ( ammoFilter.m_ammoArea && ammoFilter.m_ammoArea->IsMarked() ) + { + // "cost so far" was computed during the breadth first search within CollectSurroundingAreas() + // and is the travel distance from to this area + if ( ammoFilter.m_ammoArea->GetCostSoFar() < closestAmmoTravelDistance ) + { + closestAmmo = ammo; + closestAmmoTravelDistance = ammoFilter.m_ammoArea->GetCostSoFar(); + } + + if ( tf_bot_debug_ammo_scavenging.GetBool() ) + { + NDebugOverlay::Cross3D( ammo->WorldSpaceCenter(), 5.0f, 255, 255, 0, true, 999.9 ); + } + } + } + } + } + + // append nearby dropped weapons + CBaseEntity *ammoPack = NULL; + while( ( ammoPack = gEntList.FindEntityByClassname( ammoPack, "tf_ammo_pack" ) ) != NULL ) + { + if ( ammoFilter.IsSelected( ammoPack ) ) + { + if ( ammoFilter.m_ammoArea && ammoFilter.m_ammoArea->IsMarked() ) + { + if ( ammoFilter.m_ammoArea->GetCostSoFar() < closestAmmoTravelDistance ) + { + closestAmmo = ammoPack; + closestAmmoTravelDistance = ammoFilter.m_ammoArea->GetCostSoFar(); + } + + if ( tf_bot_debug_ammo_scavenging.GetBool() ) + { + NDebugOverlay::Cross3D( ammoPack->WorldSpaceCenter(), 5.0f, 255, 100, 0, true, 999.9 ); + } + } + } + } + + if ( !closestAmmo ) + { + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + Warning( "%3.2f: No ammo nearby\n", gpGlobals->curtime ); + } + return false; + } + + s_possibleBot = me; + s_possibleAmmo = closestAmmo; + s_possibleFrame = gpGlobals->framecount; + + return true; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotGetAmmo::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + VPROF_BUDGET( "CTFBotGetAmmo::OnStart", "NextBot" ); + + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + // if IsPossible() has already been called, use its cached data + if ( s_possibleFrame != gpGlobals->framecount || s_possibleBot != me ) + { + if ( !IsPossible( me ) || s_possibleAmmo == NULL ) + { + return Done( "Can't get ammo" ); + } + } + + m_ammo = s_possibleAmmo; + m_isGoalDispenser = m_ammo->ClassMatches( "obj_dispenser*" ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + if ( !m_path.Compute( me, m_ammo->WorldSpaceCenter(), cost ) ) + { + return Done( "No path to ammo!" ); + } + + // if I'm a spy, cloak and disguise + if ( me->IsPlayerClass( TF_CLASS_SPY ) ) + { + if ( !me->m_Shared.IsStealthed() ) + { + me->PressAltFireButton(); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotGetAmmo::Update( CTFBot *me, float interval ) +{ + if ( me->IsAmmoFull() ) + { + return Done( "My ammo is full" ); + } + + if ( m_ammo == NULL ) // || ( m_ammo->IsEffectActive( EF_NODRAW ) && !FClassnameIs( m_ammo, "func_regenerate" ) ) ) + { +/* + // engineers try to gather all the metal they can + if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) && CTFBotGetAmmo::IsPossible( me ) ) + { + // more ammo to be had + return ChangeTo( new CTFBotGetAmmo, "Not full yet - grabbing more ammo" ); + } +*/ + + return Done( "Ammo I was going for has been taken" ); + } + + if ( m_isGoalDispenser ) + { + // we need to get near and wait, not try to run over + const float nearRange = 75.0f; + if ( ( me->GetAbsOrigin() - m_ammo->GetAbsOrigin() ).IsLengthLessThan( nearRange ) ) + { + if ( me->GetVisionInterface()->IsLineOfSightClearToEntity( m_ammo ) ) + { + if ( me->IsAmmoFull() ) + { + return Done( "Ammo refilled by the Dispenser" ); + } + + // don't wait if I'm in combat + if ( !me->IsAmmoLow() && me->GetVisionInterface()->GetPrimaryKnownThreat() ) + { + return Done( "No time to wait for more ammo, I must fight" ); + } + + // wait until the dispenser refills us + return Continue(); + } + } + } + + if ( !m_path.IsValid() ) + { + return Done( "My path became invalid" ); + } + +/* TODO: Rethink this. Currently creates zombie behavior loop. + // if the closest player to the item we're after is an enemy, give up + CClosestTFPlayer close( m_ammo ); + ForEachPlayer( close ); + if ( close.m_closePlayer && !me->InSameTeam( close.m_closePlayer ) ) + return Done( "An enemy is closer to it" ); +*/ + + // may need to switch weapons due to out of ammo + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + me->EquipBestWeaponForThreat( threat ); + + m_path.Update( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGetAmmo::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGetAmmo::OnStuck( CTFBot *me ) +{ + return TryDone( RESULT_CRITICAL, "Stuck trying to reach ammo" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGetAmmo::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGetAmmo::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + return TryDone( RESULT_CRITICAL, "Failed to reach ammo" ); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotGetAmmo::ShouldHurry( const INextBot *me ) const +{ + // if we need ammo, we best hustle + return ANSWER_YES; +} diff --git a/game/server/tf/bot/behavior/tf_bot_get_ammo.h b/game/server/tf/bot/behavior/tf_bot_get_ammo.h new file mode 100644 index 0000000..f77f293 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_get_ammo.h @@ -0,0 +1,38 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_get_ammo.h +// Pick up any nearby ammo +// Michael Booth, May 2009 + +#ifndef TF_BOT_GET_AMMO_H +#define TF_BOT_GET_AMMO_H + +#include "tf_powerup.h" + +class CTFBotGetAmmo : public Action< CTFBot > +{ +public: + CTFBotGetAmmo( void ); + + static bool IsPossible( CTFBot *me ); // return true if this Action has what it needs to perform right now + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + + virtual const char *GetName( void ) const { return "GetAmmo"; }; + +private: + PathFollower m_path; + CHandle< CBaseEntity > m_ammo; + bool m_isGoalDispenser; +}; + + +#endif // TF_BOT_GET_AMMO_H diff --git a/game/server/tf/bot/behavior/tf_bot_get_health.cpp b/game/server/tf/bot/behavior/tf_bot_get_health.cpp new file mode 100644 index 0000000..91e51ef --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_get_health.cpp @@ -0,0 +1,324 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_get_health.h +// Pick up any nearby health kit +// Michael Booth, May 2009 + +#include "cbase.h" +#include "tf_gamerules.h" +#include "tf_obj.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_get_health.h" + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_health_critical_ratio( "tf_bot_health_critical_ratio", "0.3", FCVAR_CHEAT ); +ConVar tf_bot_health_ok_ratio( "tf_bot_health_ok_ratio", "0.8", FCVAR_CHEAT ); +ConVar tf_bot_health_search_near_range( "tf_bot_health_search_near_range", "1000", FCVAR_CHEAT ); +ConVar tf_bot_health_search_far_range( "tf_bot_health_search_far_range", "2000", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +class CHealthFilter : public INextBotFilter +{ +public: + CHealthFilter( CTFBot *me ) + { + m_me = me; + } + + bool IsSelected( const CBaseEntity *constCandidate ) const + { + if ( !constCandidate ) + return false; + + CBaseEntity *candidate = const_cast< CBaseEntity * >( constCandidate ); + + CTFNavArea *area = (CTFNavArea *)TheNavMesh->GetNearestNavArea( candidate->WorldSpaceCenter() ); + if ( !area ) + return false; + + CClosestTFPlayer close( candidate ); + ForEachPlayer( close ); + + // if the closest player to this candidate object is an enemy, don't use it + if ( close.m_closePlayer && !m_me->InSameTeam( close.m_closePlayer ) ) + return false; + + // resupply cabinets (not assigned a team) + if ( candidate->ClassMatches( "func_regenerate" ) ) + { + if ( !area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) ) + { + // Assume any resupply cabinets not in a teamed spawn room are inaccessible. + // Ex: pl_upward has forward spawn rooms that neither team can use until + // certain checkpoints are reached. + return false; + } + + if ( ( m_me->GetTeamNumber() == TF_TEAM_RED && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) ) || + ( m_me->GetTeamNumber() == TF_TEAM_BLUE && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) ) + { + // the supply cabinet is in my spawn room + return true; + } + + return false; + } + + // ignore non-existent ammo to ensure we collect nearby existing ammo + if ( candidate->IsEffectActive( EF_NODRAW ) ) + return false; + + if ( candidate->ClassMatches( "item_healthkit*" ) ) + return true; + + if ( m_me->InSameTeam( candidate ) ) + { + // friendly engineer's dispenser + if ( candidate->ClassMatches( "obj_dispenser*" ) ) + { + CBaseObject *dispenser = (CBaseObject *)candidate; + if ( !dispenser->IsBuilding() && !dispenser->IsPlacing() && !dispenser->IsDisabled() ) + { + return true; + } + } + } + + return false; + } + + CTFBot *m_me; +}; + + +//--------------------------------------------------------------------------------------------- +static CTFBot *s_possibleBot = NULL; +static CHandle< CBaseEntity > s_possibleHealth = NULL; +static int s_possibleFrame = 0; + + +//--------------------------------------------------------------------------------------------- +/** + * Return true if this Action has what it needs to perform right now + */ +bool CTFBotGetHealth::IsPossible( CTFBot *me ) +{ + VPROF_BUDGET( "CTFBotGetHealth::IsPossible", "NextBot" ); + + // don't move to fetch health if we have a healer + if ( me->m_Shared.GetNumHealers() > 0 ) + return false; + +#ifdef TF_RAID_MODE + // mobs don't heal + if ( TFGameRules()->IsRaidMode() && me->HasAttribute( CTFBot::AGGRESSIVE ) ) + { + return false; + } +#endif // TF_RAID_MODE + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + return false; + } + + float healthRatio = (float)me->GetHealth() / (float)me->GetMaxHealth(); + + float t = ( healthRatio - tf_bot_health_critical_ratio.GetFloat() ) / ( tf_bot_health_ok_ratio.GetFloat() - tf_bot_health_critical_ratio.GetFloat() ); + t = clamp( t, 0.0f, 1.0f ); + + if ( me->m_Shared.InCond( TF_COND_BURNING ) ) + { + // on fire - get health now + t = 0.0f; + } + + // the more we are hurt, the farther we'll travel to get health + float searchRange = tf_bot_health_search_far_range.GetFloat() + t * ( tf_bot_health_search_near_range.GetFloat() - tf_bot_health_search_far_range.GetFloat() ); + + CUtlVector< CHandle< CBaseEntity > > healthVector; + CHealthFilter healthFilter( me ); + + me->SelectReachableObjects( TFGameRules()->GetHealthEntityVector(), &healthVector, healthFilter, me->GetLastKnownArea(), searchRange ); + + if ( healthVector.Count() == 0 ) + { + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + Warning( "%3.2f: No health nearby\n", gpGlobals->curtime ); + } + return false; + } + + // use the first item in the list, since it will be the closest to us (or nearly so) + CBaseEntity *health = healthVector[0]; + for( int i=0; i<healthVector.Count(); ++i ) + { + if ( healthVector[i]->GetTeamNumber() != GetEnemyTeam( me->GetTeamNumber() ) ) + { + health = healthVector[i]; + break; + } + } + + if ( health == NULL ) + { + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + Warning( "%3.2f: No health available to my team nearby\n", gpGlobals->curtime ); + } + return false; + } + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + PathFollower path; + if ( !path.Compute( me, health->WorldSpaceCenter(), cost ) ) + { + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + Warning( "%3.2f: No path to health!\n", gpGlobals->curtime ); + } + return false; + } + + s_possibleBot = me; + s_possibleHealth = health; + s_possibleFrame = gpGlobals->framecount; + + return true; +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotGetHealth::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + VPROF_BUDGET( "CTFBotGetHealth::OnStart", "NextBot" ); + + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + // if IsPossible() has already been called, use its cached data + if ( s_possibleFrame != gpGlobals->framecount || s_possibleBot != me ) + { + if ( !IsPossible( me ) || s_possibleHealth == NULL ) + { + return Done( "Can't get health" ); + } + } + + m_healthKit = s_possibleHealth; + m_isGoalDispenser = m_healthKit->ClassMatches( "obj_dispenser*" ); + + CTFBotPathCost cost( me, SAFEST_ROUTE ); + if ( !m_path.Compute( me, m_healthKit->WorldSpaceCenter(), cost ) ) + { + return Done( "No path to health!" ); + } + + // if I'm a spy, cloak and disguise + if ( me->IsPlayerClass( TF_CLASS_SPY ) ) + { + if ( !me->m_Shared.IsStealthed() ) + { + me->PressAltFireButton(); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotGetHealth::Update( CTFBot *me, float interval ) +{ + if ( m_healthKit == NULL || ( m_healthKit->IsEffectActive( EF_NODRAW ) && !FClassnameIs( m_healthKit, "func_regenerate" ) ) ) + { + return Done( "Health kit I was going for has been taken" ); + } + + // if a medic is healing us, give up on getting a kit + int i; + for( i=0; i<me->m_Shared.GetNumHealers(); ++i ) + { + if ( !me->m_Shared.HealerIsDispenser( i ) ) + break; + } + + if ( i < me->m_Shared.GetNumHealers() ) + { + return Done( "A Medic is healing me" ); + } + + if ( me->m_Shared.GetNumHealers() ) + { + // a dispenser is healing me, don't wait if I'm in combat + const CKnownEntity *known = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( known && known->IsVisibleInFOVNow() ) + { + return Done( "No time to wait for health, I must fight" ); + } + } + + if ( me->GetHealth() >= me->GetMaxHealth() ) + { + return Done( "I've been healed" ); + } + + // if the closest player to the item we're after is an enemy, give up + CClosestTFPlayer close( m_healthKit ); + ForEachPlayer( close ); + if ( close.m_closePlayer && !me->InSameTeam( close.m_closePlayer ) ) + return Done( "An enemy is closer to it" ); + + // un-zoom + CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon(); + if ( myWeapon && myWeapon->IsWeapon( TF_WEAPON_SNIPERRIFLE ) && me->m_Shared.InCond( TF_COND_ZOOMED ) ) + me->PressAltFireButton(); + + if ( !m_path.IsValid() ) + { + // this can occur if we overshoot the health kit's location + // because it is momentarily gone + CTFBotPathCost cost( me, SAFEST_ROUTE ); + if ( !m_path.Compute( me, m_healthKit->WorldSpaceCenter(), cost ) ) + { + return Done( "No path to health!" ); + } + } + + m_path.Update( me ); + + // may need to switch weapons (ie: engineer holding toolbox now needs to heal and defend himself) + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + me->EquipBestWeaponForThreat( threat ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGetHealth::OnStuck( CTFBot *me ) +{ + return TryDone( RESULT_CRITICAL, "Stuck trying to reach health kit" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGetHealth::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotGetHealth::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + return TryDone( RESULT_CRITICAL, "Failed to reach health kit" ); +} + + +//--------------------------------------------------------------------------------------------- +// We are always hurrying if we need to collect health +QueryResultType CTFBotGetHealth::ShouldHurry( const INextBot *me ) const +{ + return ANSWER_YES; +} diff --git a/game/server/tf/bot/behavior/tf_bot_get_health.h b/game/server/tf/bot/behavior/tf_bot_get_health.h new file mode 100644 index 0000000..99d4d1d --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_get_health.h @@ -0,0 +1,34 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_get_health.h +// Pick up any nearby health kit +// Michael Booth, May 2009 + +#ifndef TF_BOT_GET_HEALTH_H +#define TF_BOT_GET_HEALTH_H + +#include "tf_powerup.h" + +class CTFBotGetHealth : public Action< CTFBot > +{ +public: + static bool IsPossible( CTFBot *me ); // Return true if this Action has what it needs to perform right now + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + + virtual const char *GetName( void ) const { return "GetHealth"; }; + +private: + PathFollower m_path; + CHandle< CTFPowerup > m_healthKit; + bool m_isGoalDispenser; +}; + + +#endif // TF_BOT_GET_HEALTH_H diff --git a/game/server/tf/bot/behavior/tf_bot_melee_attack.cpp b/game/server/tf/bot/behavior/tf_bot_melee_attack.cpp new file mode 100644 index 0000000..c914a8c --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_melee_attack.cpp @@ -0,0 +1,67 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_melee_attack.h +// Attack a threat with out melee weapon +// Michael Booth, February 2009 + +#include "cbase.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_melee_attack.h" + +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; + +ConVar tf_bot_melee_attack_abandon_range( "tf_bot_melee_attack_abandon_range", "500", FCVAR_CHEAT, "If threat is farther away than this, bot will switch back to its primary weapon and attack" ); + + +//--------------------------------------------------------------------------------------------- +CTFBotMeleeAttack::CTFBotMeleeAttack( float giveUpRange ) +{ + m_giveUpRange = giveUpRange < 0.0f ? tf_bot_melee_attack_abandon_range.GetFloat() : giveUpRange; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMeleeAttack::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMeleeAttack::Update( CTFBot *me, float interval ) +{ + // bash the bad guys + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + + if ( threat == NULL ) + { + return Done( "No threat" ); + } + + if ( me->IsDistanceBetweenGreaterThan( threat->GetLastKnownPosition(), m_giveUpRange ) ) + { + // threat is too far away for melee + return Done( "Threat is too far away for a melee attack" ); + } + + // switch to our melee weapon + CBaseCombatWeapon *meleeWeapon = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE ); + if ( meleeWeapon ) + { + me->Weapon_Switch( meleeWeapon ); + } + + // actual head aiming is handled elsewhere + + // just keep swinging + me->PressFireButton(); + + // chase them down + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Update( me, threat->GetEntity(), cost ); + + return Continue(); +} diff --git a/game/server/tf/bot/behavior/tf_bot_melee_attack.h b/game/server/tf/bot/behavior/tf_bot_melee_attack.h new file mode 100644 index 0000000..fc47fd0 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_melee_attack.h @@ -0,0 +1,26 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_melee_attack.h +// Attack a threat with out melee weapon +// Michael Booth, February 2009 + +#ifndef TF_BOT_MELEE_ATTACK_H +#define TF_BOT_MELEE_ATTACK_H + +#include "Path/NextBotChasePath.h" + +class CTFBotMeleeAttack : public Action< CTFBot > +{ +public: + CTFBotMeleeAttack( float giveUpRange = -1.0f ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "MeleeAttack"; }; + +private: + float m_giveUpRange; // if non-negative and if threat is farther than this, give up our melee attack + ChasePath m_path; +}; + +#endif // TF_BOT_MELEE_ATTACK_H diff --git a/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.cpp b/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.cpp new file mode 100644 index 0000000..9a620df --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.cpp @@ -0,0 +1,90 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_move_to_vantage_point.h +// Move to a position where at least one enemy is visible +// Michael Booth, November 2009 + +#include "cbase.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_move_to_vantage_point.h" + +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; + + +//--------------------------------------------------------------------------------------------- +CTFBotMoveToVantagePoint::CTFBotMoveToVantagePoint( float maxTravelDistance ) +{ + m_maxTravelDistance = maxTravelDistance; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMoveToVantagePoint::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + m_vantageArea = me->FindVantagePoint( m_maxTravelDistance ); + if ( !m_vantageArea ) + { + return Done( "No vantage point found" ); + } + + m_path.Invalidate(); + m_repathTimer.Invalidate(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMoveToVantagePoint::Update( CTFBot *me, float interval ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleInFOVNow() ) + { + return Done( "Enemy is visible" ); + } + + if ( !m_path.IsValid() && m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( 1.0f ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + if ( !m_path.Compute( me, m_vantageArea->GetCenter(), cost ) ) + { + return Done( "No path to vantage point exists" ); + } + } + + // move along path to vantage point + m_path.Update( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMoveToVantagePoint::OnStuck( CTFBot *me ) +{ + m_path.Invalidate(); + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMoveToVantagePoint::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryDone( RESULT_CRITICAL, "Vantage point reached" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMoveToVantagePoint::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + m_path.Invalidate(); + return TryContinue(); +} + + diff --git a/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.h b/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.h new file mode 100644 index 0000000..b3cb51b --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_move_to_vantage_point.h @@ -0,0 +1,33 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_move_to_vantage_point.h +// Move to a position where at least one enemy is visible +// Michael Booth, November 2009 + +#ifndef TF_BOT_MOVE_TO_VANTAGE_POINT_H +#define TF_BOT_MOVE_TO_VANTAGE_POINT_H + +#include "Path/NextBotChasePath.h" + +class CTFBotMoveToVantagePoint : public Action< CTFBot > +{ +public: + CTFBotMoveToVantagePoint( float maxTravelDistance = 2000.0f ); + virtual ~CTFBotMoveToVantagePoint() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual const char *GetName( void ) const { return "MoveToVantagePoint"; }; + +private: + float m_maxTravelDistance; + PathFollower m_path; + CountdownTimer m_repathTimer; + CTFNavArea *m_vantageArea; +}; + +#endif // TF_BOT_MOVE_TO_VANTAGE_POINT_H diff --git a/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.cpp b/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.cpp new file mode 100644 index 0000000..87880fa --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.cpp @@ -0,0 +1,190 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_mvm_deploy_bomb.cpp +// Set us up the bomb! + +#include "cbase.h" +#include "team.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_mvm_deploy_bomb.h" +#include "econ_item_system.h" + + +extern ConVar tf_deploying_bomb_delay_time; +extern ConVar tf_deploying_bomb_time; + + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMvMDeployBomb::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + me->SetDeployingBombState( TF_BOMB_DEPLOYING_DELAY ); + m_timer.Start( tf_deploying_bomb_delay_time.GetFloat() ); + + // remember where we start deploying + m_anchorPos = me->GetAbsOrigin(); + me->GetLocomotionInterface()->Stop(); + me->SetAbsVelocity( Vector( 0.0f, 0.0f, 0.0f ) ); + + if ( me->IsMiniBoss() ) + { + static CSchemaAttributeDefHandle pAttrDef_AirblastVerticalVulnerability( "airblast vertical vulnerability multiplier" ); + + // Minibosses can't be pushed once they start deploying + if ( !pAttrDef_AirblastVerticalVulnerability ) + { + Warning( "TFBotSpawner: Invalid attribute 'airblast vertical vulnerability multiplier'\n" ); + } + else + { + me->GetAttributeList()->SetRuntimeAttributeValue( pAttrDef_AirblastVerticalVulnerability, 0.0f ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotMvMDeployBomb::Update( CTFBot *me, float interval ) +{ + CCaptureZone *pAreaTrigger = NULL; + + if ( me->GetDeployingBombState() != TF_BOMB_DEPLOYING_COMPLETE ) + { + pAreaTrigger = me->GetClosestCaptureZone(); + if ( !pAreaTrigger ) + { + return Done( "No capture zone!" ); + } + + // if we've been moved, give up and go back to normal behavior + const float movedRange = 20.0f; + if ( me->IsRangeGreaterThan( m_anchorPos, movedRange ) ) + { + // Look for players that pushed me away and send an event + CUtlVector<CTFPlayer *> playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS ); + FOR_EACH_VEC( playerVector, i ) + { + CTFPlayer *pPlayer = playerVector[i]; + if ( !pPlayer ) + continue; + + if ( me->m_AchievementData.IsPusherInHistory( pPlayer, 2.f ) ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_bomb_deploy_reset_by_player" ); + if ( event ) + { + event->SetInt( "player", pPlayer->entindex() ); + gameeventmanager->FireEvent( event ); + } + } + } + + return Done( "I've been pushed" ); + } + + // face the capture zone + me->GetBodyInterface()->AimHeadTowards( pAreaTrigger->WorldSpaceCenter(), IBody::CRITICAL, 0.5f, NULL, "Face point for bomb deploy" ); + + // slam facing towards bomb hole + Vector to = pAreaTrigger->WorldSpaceCenter() - me->WorldSpaceCenter(); + to.NormalizeInPlace(); + + QAngle desiredAngles; + VectorAngles( to, desiredAngles ); + + me->SnapEyeAngles( desiredAngles ); + } + + switch ( me->GetDeployingBombState() ) + { + case TF_BOMB_DEPLOYING_DELAY: + if ( m_timer.IsElapsed() ) + { + me->PlaySpecificSequence( "primary_deploybomb" ); + m_timer.Start( tf_deploying_bomb_time.GetFloat() ); + me->SetDeployingBombState( TF_BOMB_DEPLOYING_ANIMATING ); + + const char *pszSoundName = me->IsMiniBoss() ? "MVM.DeployBombGiant" : "MVM.DeployBombSmall"; + me->EmitSound( pszSoundName ); + + TFGameRules()->PlayThrottledAlert( 255, "Announcer.MVM_Bomb_Alert_Deploying", 5.0f ); + } + break; + + case TF_BOMB_DEPLOYING_ANIMATING: + if ( m_timer.IsElapsed() ) + { + if ( pAreaTrigger ) + { + pAreaTrigger->Capture( me ); + } + + m_timer.Start( 2.0f ); + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Robots_Planted" ); + me->SetDeployingBombState( TF_BOMB_DEPLOYING_COMPLETE ); + me->m_takedamage = DAMAGE_NO; + me->AddEffects( EF_NODRAW ); + me->RemoveAllWeapons(); + } + break; + + case TF_BOMB_DEPLOYING_COMPLETE: + if ( m_timer.IsElapsed() ) + { + me->SetDeployingBombState( TF_BOMB_DEPLOYING_NONE ); + me->m_takedamage = DAMAGE_YES; + me->TakeDamage( CTakeDamageInfo( me, me, 99999.9f, DMG_CRUSH ) ); + return Done( "I've deployed successfully" ); + } + break; + } + + return Continue(); +} + + +extern void TE_PlayerAnimEvent( CBasePlayer *pPlayer, PlayerAnimEvent_t event, int nData ); + + +//--------------------------------------------------------------------------------------------- +void CTFBotMvMDeployBomb::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + if ( me->GetDeployingBombState() == TF_BOMB_DEPLOYING_ANIMATING ) + { + // reset the in-progress deploy animation + me->m_PlayerAnimState->DoAnimationEvent( PLAYERANIMEVENT_SPAWN ); + TE_PlayerAnimEvent( me, PLAYERANIMEVENT_SPAWN, 0 ); // Send to any clients who can see this guy. + } + + if ( me->IsMiniBoss() ) + { + static CSchemaAttributeDefHandle pAttrDef_AirblastVerticalVulnerability( "airblast vertical vulnerability multiplier" ); + + // Minibosses can be pushed again + if ( !pAttrDef_AirblastVerticalVulnerability ) + { + Warning( "TFBotSpawner: Invalid attribute 'airblast vertical vulnerability multiplier'\n" ); + } + else + { + me->GetAttributeList()->RemoveAttribute( pAttrDef_AirblastVerticalVulnerability ); + } + } + + me->SetDeployingBombState( TF_BOMB_DEPLOYING_NONE ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotMvMDeployBomb::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result ) +{ + // so event doesn't fall thru to buried action which will then redo transition to this state as we stay in contact with the zone + return TryToSustain( RESULT_CRITICAL ); +} + +QueryResultType CTFBotMvMDeployBomb::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_NO; +} diff --git a/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.h b/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.h new file mode 100644 index 0000000..ae3043c --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_mvm_deploy_bomb.h @@ -0,0 +1,27 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_mvm_deploy_bomb.h +// Set us up the bomb! + +#ifndef TF_BOT_MVM_DEPLOY_BOMB_H +#define TF_BOT_MVM_DEPLOY_BOMB_H + +//----------------------------------------------------------------------------- +class CTFBotMvMDeployBomb : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + EventDesiredResult< CTFBot > OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result ); + QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; + + virtual const char *GetName( void ) const { return "MvMDeployBomb"; }; + +private: + CountdownTimer m_timer; + Vector m_anchorPos; +}; + + +#endif // TF_BOT_MVM_DEPLOY_BOMB_H diff --git a/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.cpp b/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.cpp new file mode 100644 index 0000000..5dd970b --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.cpp @@ -0,0 +1,317 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_move_to_cover.cpp +// Retreat to local cover from known threats +// Michael Booth, June 2009 + +#include "cbase.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_retreat_to_cover.h" + +extern ConVar tf_bot_path_lookahead_range; +ConVar tf_bot_retreat_to_cover_range( "tf_bot_retreat_to_cover_range", "1000", FCVAR_CHEAT ); +ConVar tf_bot_debug_retreat_to_cover( "tf_bot_debug_retreat_to_cover", "0", FCVAR_CHEAT ); +ConVar tf_bot_wait_in_cover_min_time( "tf_bot_wait_in_cover_min_time", "1", FCVAR_CHEAT ); +ConVar tf_bot_wait_in_cover_max_time( "tf_bot_wait_in_cover_max_time", "2", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +CTFBotRetreatToCover::CTFBotRetreatToCover( float hideDuration ) +{ + m_hideDuration = hideDuration; + m_actionToChangeToOnceCoverReached = NULL; +} + + +//--------------------------------------------------------------------------------------------- +CTFBotRetreatToCover::CTFBotRetreatToCover( Action< CTFBot > *actionToChangeToOnceCoverReached ) +{ + m_hideDuration = -1.0f; + m_actionToChangeToOnceCoverReached = actionToChangeToOnceCoverReached; +} + + +//--------------------------------------------------------------------------------------------- +// for testing a given area's exposure to known threats +class CTestAreaAgainstThreats : public IVision::IForEachKnownEntity +{ +public: + CTestAreaAgainstThreats( CTFBot *me, CTFNavArea *area ) + { + m_me = me; + m_area = area; + m_exposedThreatCount = 0; + } + + virtual bool Inspect( const CKnownEntity &known ) + { + VPROF_BUDGET( "CTestAreaAgainstThreats::Inspect", "NextBot" ); + + if ( m_me->IsEnemy( known.GetEntity() ) ) + { + const CNavArea *threatArea = known.GetLastKnownArea(); + + if ( threatArea ) + { + // is area visible by known threat + if ( m_area->IsPotentiallyVisible( threatArea ) ) + ++m_exposedThreatCount; + } + } + + return true; + } + + CTFBot *m_me; + CTFNavArea *m_area; + int m_exposedThreatCount; +}; + + +// collect nearby areas that provide cover from our known threats +class CSearchForCover : public ISearchSurroundingAreasFunctor +{ +public: + CSearchForCover( CTFBot *me ) + { + m_me = me; + m_minExposureCount = 9999; + + if ( tf_bot_debug_retreat_to_cover.GetBool() ) + TheNavMesh->ClearSelectedSet(); + } + + virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) + { + VPROF_BUDGET( "CSearchForCover::operator()", "NextBot" ); + + CTFNavArea *area = (CTFNavArea *)baseArea; + + CTestAreaAgainstThreats test( m_me, area ); + m_me->GetVisionInterface()->ForEachKnownEntity( test ); + + if ( test.m_exposedThreatCount <= m_minExposureCount ) + { + // this area is at least as good as already found cover + if ( test.m_exposedThreatCount < m_minExposureCount ) + { + // this area is better than already found cover - throw out list and start over + m_coverAreaVector.RemoveAll(); + m_minExposureCount = test.m_exposedThreatCount; + } + + m_coverAreaVector.AddToTail( area ); + } + + return true; + } + + // return true if 'adjArea' should be included in the ongoing search + virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar ) + { + if ( travelDistanceSoFar > tf_bot_retreat_to_cover_range.GetFloat() ) + return false; + + // allow falling off ledges, but don't jump up - too slow + return ( currentArea->ComputeAdjacentConnectionHeightChange( adjArea ) < m_me->GetLocomotionInterface()->GetStepHeight() ); + } + + virtual void PostSearch( void ) + { + if ( tf_bot_debug_retreat_to_cover.GetBool() ) + { + for( int i=0; i<m_coverAreaVector.Count(); ++i ) + TheNavMesh->AddToSelectedSet( m_coverAreaVector[i] ); + } + } + + CTFBot *m_me; + CUtlVector< CTFNavArea * > m_coverAreaVector; + int m_minExposureCount; +}; + + +//--------------------------------------------------------------------------------------------- +CTFNavArea *CTFBotRetreatToCover::FindCoverArea( CTFBot *me ) +{ + VPROF_BUDGET( "CTFBotRetreatToCover::FindCoverArea", "NextBot" ); + + CSearchForCover search( me ); + SearchSurroundingAreas( me->GetLastKnownArea(), search ); + + if ( search.m_coverAreaVector.Count() == 0 ) + { + return NULL; + } + + // first in vector should be closest via travel distance + // pick from the closest 10 areas to avoid the whole team bunching up in one spot + int last = MIN( 10, search.m_coverAreaVector.Count() ); + int which = RandomInt( 0, last-1 ); + return search.m_coverAreaVector[ which ]; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotRetreatToCover::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + m_coverArea = FindCoverArea( me ); + + if ( m_coverArea == NULL ) + return Done( "No cover available!" ); + + if ( m_hideDuration < 0.0f ) + { + m_hideDuration = RandomFloat( tf_bot_wait_in_cover_min_time.GetFloat(), tf_bot_wait_in_cover_max_time.GetFloat() ); + } + + m_waitInCoverTimer.Start( m_hideDuration ); + + // if I'm a spy, cloak and disguise while I retreat + if ( me->IsPlayerClass( TF_CLASS_SPY ) ) + { + if ( !me->m_Shared.IsStealthed() ) + { + me->PressAltFireButton(); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotRetreatToCover::Update( CTFBot *me, float interval ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat( true ); + + if ( me->m_Shared.InCond( TF_COND_INVULNERABLE ) ) + return Done( "I'm invulnerable - no need to retreat!" ); + + if ( ShouldRetreat( me ) == ANSWER_NO ) + return Done( "No longer need to retreat" ); + + // attack while retreating + me->EquipBestWeaponForThreat( threat ); + + // reload while moving to cover + bool isDoingAFullReload = false; + CTFWeaponBase *myPrimary = (CTFWeaponBase *)me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ); + if ( myPrimary && me->GetAmmoCount( TF_AMMO_PRIMARY ) > 0 && me->IsBarrageAndReloadWeapon( myPrimary ) ) + { + if ( myPrimary->Clip1() < myPrimary->GetMaxClip1() ) + { + me->PressReloadButton(); + isDoingAFullReload = true; + } + } + + + // move to cover, or stop if we've found opportunistic cover (no visible threats right now) + if ( me->GetLastKnownArea() == m_coverArea || !threat ) + { + // we are now in cover + + if ( threat ) + { + // threats are still visible - find new cover + m_coverArea = FindCoverArea( me ); + + if ( m_coverArea == NULL ) + { + return Done( "My cover is exposed, and there is no other cover available!" ); + } + } + + if ( me->IsPlayerClass( TF_CLASS_SPY ) && !me->m_Shared.InCond( TF_COND_DISGUISED ) ) + { + // don't leave cover until my disguise kicks in + return Continue(); + } + + // uncloak so we can attack when we leave cover + if ( me->m_Shared.IsStealthed() ) + { + me->PressAltFireButton(); + } + + if ( m_actionToChangeToOnceCoverReached ) + { + return ChangeTo( m_actionToChangeToOnceCoverReached, "Doing given action now that I'm in cover" ); + } + + // if I'm being healed by a medic who nearly has his charge built up, wait in cover until his charge is ready + int numHealers = me->m_Shared.GetNumHealers(); + for ( int i=0; i<numHealers; ++i ) + { + CTFPlayer *medic = ToTFPlayer( me->m_Shared.GetHealerByIndex( i ) ); + + if ( medic && medic->MedicGetChargeLevel() > 0.9f ) + { + // wait for uber to finish + return Continue(); + } + } + + // stay in cover while we fully reload + if ( isDoingAFullReload ) + { + return Continue(); + } + + if ( m_waitInCoverTimer.IsElapsed() ) + { + return Done( "Been in cover long enough" ); + } + } + else + { + // not in cover yet + m_waitInCoverTimer.Reset(); + + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); + + CTFBotPathCost cost( me, RETREAT_ROUTE ); + m_path.Compute( me, m_coverArea->GetCenter(), cost ); + } + + m_path.Update( me ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotRetreatToCover::OnStuck( CTFBot *me ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotRetreatToCover::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotRetreatToCover::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +// Hustle yer butt to safety! +QueryResultType CTFBotRetreatToCover::ShouldHurry( const INextBot *me ) const +{ + return ANSWER_YES; +} + + diff --git a/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.h b/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.h new file mode 100644 index 0000000..799a4af --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_retreat_to_cover.h @@ -0,0 +1,41 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_retreat_to_cover.h +// Retreat to local cover from known threats +// Michael Booth, June 2009 + +#ifndef TF_BOT_RETREAT_TO_COVER_H +#define TF_BOT_RETREAT_TO_COVER_H + +class CTFBotRetreatToCover : public Action< CTFBot > +{ +public: + CTFBotRetreatToCover( float hideDuration = -1.0f ); + CTFBotRetreatToCover( Action< CTFBot > *actionToChangeToOnceCoverReached ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + + virtual const char *GetName( void ) const { return "RetreatToCover"; }; + +private: + float m_hideDuration; + Action< CTFBot > *m_actionToChangeToOnceCoverReached; + + PathFollower m_path; + CountdownTimer m_repathTimer; + + CTFNavArea *m_coverArea; + CountdownTimer m_waitInCoverTimer; + + CTFNavArea *FindCoverArea( CTFBot *me ); +}; + + + +#endif // TF_BOT_RETREAT_TO_COVER_H diff --git a/game/server/tf/bot/behavior/tf_bot_scenario_monitor.cpp b/game/server/tf/bot/behavior/tf_bot_scenario_monitor.cpp new file mode 100644 index 0000000..50ed460 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_scenario_monitor.cpp @@ -0,0 +1,368 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_scenario_monitor.h +// Behavior layer that interrupts for scenario rules (picked up flag, drop what you're doing and capture, etc) +// Michael Booth, May 2011 + +#include "cbase.h" +#include "fmtstr.h" + +#include "tf_gamerules.h" +#include "tf_weapon_pipebomblauncher.h" +#include "NextBot/NavMeshEntities/func_nav_prerequisite.h" + +#include "bot/tf_bot.h" +#include "bot/tf_bot_manager.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_retreat_to_cover.h" +#include "bot/behavior/tf_bot_get_health.h" +#include "bot/behavior/tf_bot_get_ammo.h" +#include "bot/behavior/sniper/tf_bot_sniper_lurk.h" +#include "bot/behavior/scenario/capture_point/tf_bot_capture_point.h" +#include "bot/behavior/scenario/capture_point/tf_bot_defend_point.h" +#include "bot/behavior/scenario/payload/tf_bot_payload_guard.h" +#include "bot/behavior/scenario/payload/tf_bot_payload_push.h" +#include "bot/behavior/tf_bot_use_teleporter.h" +#include "bot/behavior/training/tf_bot_training.h" +#include "bot/behavior/tf_bot_destroy_enemy_sentry.h" +#include "bot/behavior/engineer/tf_bot_engineer_building.h" +#include "bot/behavior/spy/tf_bot_spy_infiltrate.h" +#include "bot/behavior/spy/tf_bot_spy_leave_spawn_room.h" +#include "bot/behavior/medic/tf_bot_medic_heal.h" +#include "bot/behavior/engineer/tf_bot_engineer_build.h" +#include "bot/map_entities/tf_bot_hint_sentrygun.h" + +#ifdef TF_RAID_MODE +#include "bot/behavior/scenario/raid/tf_bot_wander.h" +#include "bot/behavior/scenario/raid/tf_bot_companion.h" +#include "bot/behavior/scenario/raid/tf_bot_squad_attack.h" +#include "bot/behavior/scenario/raid/tf_bot_guard_area.h" +#endif // TF_RAID_MODE + +#include "bot/behavior/tf_bot_attack.h" +#include "bot/behavior/tf_bot_seek_and_destroy.h" +#include "bot/behavior/tf_bot_taunt.h" +#include "bot/behavior/tf_bot_escort.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h" +#include "bot/behavior/scenario/capture_the_flag/tf_bot_deliver_flag.h" + +#include "bot/behavior/missions/tf_bot_mission_suicide_bomber.h" +#include "bot/behavior/squad/tf_bot_escort_squad_leader.h" +#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h" +#include "bot/behavior/missions/tf_bot_mission_reprogrammed.h" + +#include "bot/behavior/tf_bot_scenario_monitor.h" + + +extern ConVar tf_bot_health_ok_ratio; +extern ConVar tf_bot_health_critical_ratio; + + +//----------------------------------------------------------------------------------------- +// Returns the initial Action we will run concurrently as a child to us +Action< CTFBot > *CTFBotScenarioMonitor::InitialContainedAction( CTFBot *me ) +{ + if ( me->IsInASquad() ) + { + if ( me->GetSquad()->IsLeader( me ) ) + { + // I'm the leader of this Squad, so I can do what I want and the other Squaddies will support me + return DesiredScenarioAndClassAction( me ); + } + + // Medics are the exception - they always heal, and have special squad logic in their heal logic + if ( me->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + return new CTFBotMedicHeal; + } + + // I'm in a Squad but not the leader, do "escort and support" Squad behavior + // until the Squad disbands, and then do my normal thing + return new CTFBotEscortSquadLeader( DesiredScenarioAndClassAction( me ) ); + } + + return DesiredScenarioAndClassAction( me ); +} + + +//----------------------------------------------------------------------------------------- +// Returns Action specific to the scenario and my class +Action< CTFBot > *CTFBotScenarioMonitor::DesiredScenarioAndClassAction( CTFBot *me ) +{ + switch( me->GetMission() ) + { + case CTFBot::MISSION_SEEK_AND_DESTROY: + break; + + case CTFBot::MISSION_DESTROY_SENTRIES: + return new CTFBotMissionSuicideBomber; + + case CTFBot::MISSION_SNIPER: + return new CTFBotSniperLurk; + +#ifdef STAGING_ONLY + case CTFBot::MISSION_REPROGRAMMED: + return new CTFBotMissionReprogrammed; +#endif + } + +#ifdef TF_RAID_MODE + if ( me->HasAttribute( CTFBot::IS_NPC ) ) + { + // map-spawned guardians + return new CTFBotGuardian; + } +#endif // TF_RAID_MODE + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsBossBattleMode() ) + { + if ( me->GetTeamNumber() == TF_TEAM_BLUE ) + { + // bot teammates + return new CTFBotCompanion; + } + + if ( me->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + return new CTFBotSniperLurk; + } + + if ( me->IsPlayerClass( TF_CLASS_SPY ) ) + { + return new CTFBotSpyInfiltrate; + } + + if ( me->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + return new CTFBotMedicHeal; + } + + if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + return new CTFBotEngineerBuild; + } + + return new CTFBotEscort( TFGameRules()->GetActiveBoss() ); + } + else if ( TFGameRules()->IsRaidMode() ) + { + if ( me->GetTeamNumber() == TF_TEAM_BLUE ) + { + // bot teammates + return new CTFBotCompanion; + } + + if ( me->IsInASquad() ) + { + // squad behavior + return new CTFBotSquadAttack; + } + + if ( me->IsPlayerClass( TF_CLASS_SCOUT ) || me->HasAttribute( CTFBot::AGGRESSIVE ) ) + { + return new CTFBotWander; + } + + if ( me->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + return new CTFBotSniperLurk; + } + + if ( me->IsPlayerClass( TF_CLASS_SPY ) ) + { + return new CTFBotSpyInfiltrate; + } + + return new CTFBotGuardArea; + } +#endif // TF_RAID_MODE + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + if ( me->IsPlayerClass( TF_CLASS_SPY ) ) + { + return new CTFBotSpyLeaveSpawnRoom; + } + + if ( me->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + // if I'm being healed by another medic, I should do something else other than healing + bool bIsBeingHealedByAMedic = false; + int nNumHealers = me->m_Shared.GetNumHealers(); + for ( int i=0; i<nNumHealers; ++i ) + { + CBaseEntity *pHealer = me->m_Shared.GetHealerByIndex(i); + if ( pHealer && pHealer->IsPlayer() ) + { + bIsBeingHealedByAMedic = true; + break; + } + } + + if ( !bIsBeingHealedByAMedic ) + { + return new CTFBotMedicHeal; + } + } + + if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + return new CTFBotMvMEngineerIdle; + } + + // NOTE: Snipers are intentionally left out so they go after the flag. Actual sniping behavior is done as a mission. + + if ( me->HasAttribute( CTFBot::AGGRESSIVE ) ) + { + // push for the point first, then attack + return new CTFBotPushToCapturePoint( new CTFBotFetchFlag ); + } + + // capture the flag + return new CTFBotFetchFlag; + } + + if ( me->IsPlayerClass( TF_CLASS_SPY ) ) + { + return new CTFBotSpyInfiltrate; + } + + if ( !TheTFBots().IsMeleeOnly() ) + { + if ( me->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + return new CTFBotSniperLurk; + } + + if ( me->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + return new CTFBotMedicHeal; + } + + if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + return new CTFBotEngineerBuild; + } + } + + if ( me->GetFlagToFetch() ) + { + // capture the flag + return new CTFBotFetchFlag; + } + else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT ) + { + // push the cart + if ( me->GetTeamNumber() == TF_TEAM_BLUE ) + { + // blu is pushing + return new CTFBotPayloadPush; + } + else if ( me->GetTeamNumber() == TF_TEAM_RED ) + { + // red is blocking + return new CTFBotPayloadGuard; + } + } + else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP ) + { + // if we have a point we can capture - do it + CUtlVector< CTeamControlPoint * > captureVector; + TFGameRules()->CollectCapturePoints( me, &captureVector ); + + if ( captureVector.Count() > 0 ) + { + return new CTFBotCapturePoint; + } + + // otherwise, defend our point(s) from capture + CUtlVector< CTeamControlPoint * > defendVector; + TFGameRules()->CollectDefendPoints( me, &defendVector ); + + if ( defendVector.Count() > 0 ) + { + return new CTFBotDefendPoint; + } + + // likely KotH mode and/or all points are locked - assume capture + DevMsg( "%3.2f: %s: Gametype is CP, but I can't find a point to capture or defend!\n", gpGlobals->curtime, me->GetDebugIdentifier() ); + return new CTFBotCapturePoint; + } + else + { + // scenario not implemented yet - just fight + return new CTFBotSeekAndDestroy; + } + + return NULL; +} + + +//----------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotScenarioMonitor::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_ignoreLostFlagTimer.Start( 20.0f ); + m_lostFlagTimer.Invalidate(); + return Continue(); +} + + +ConVar tf_bot_fetch_lost_flag_time( "tf_bot_fetch_lost_flag_time", "10", FCVAR_CHEAT, "How long busy TFBots will ignore the dropped flag before they give up what they are doing and go after it" ); +ConVar tf_bot_flag_kill_on_touch( "tf_bot_flag_kill_on_touch", "0", FCVAR_CHEAT, "If nonzero, any bot that picks up the flag dies. For testing." ); + + +//----------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotScenarioMonitor::Update( CTFBot *me, float interval ) +{ + // CTF Scenario + if ( me->HasTheFlag() ) + { + if ( tf_bot_flag_kill_on_touch.GetBool() ) + { + me->CommitSuicide( false, true ); + return Done( "Flag kill" ); + } + + // we just picked up the flag - drop what we're doing and take it in + return SuspendFor( new CTFBotDeliverFlag, "I've picked up the flag! Running it in..." ); + } + + if ( me->HasMission( CTFBot::NO_MISSION ) && m_ignoreLostFlagTimer.IsElapsed() && me->IsAllowedToPickUpFlag() ) + { + CCaptureFlag *flag = me->GetFlagToFetch(); + + if ( flag ) + { + CTFPlayer *carrier = ToTFPlayer( flag->GetOwnerEntity() ); + if ( carrier ) + { + m_lostFlagTimer.Invalidate(); + } + else + { + // flag is loose + if ( !m_lostFlagTimer.HasStarted() ) + { + m_lostFlagTimer.Start( tf_bot_fetch_lost_flag_time.GetFloat() ); + } + else if ( m_lostFlagTimer.IsElapsed() ) + { + m_lostFlagTimer.Invalidate(); + + // if we're a Medic an actively healing someone, don't interrupt + if ( !me->MedicGetHealTarget() ) + { + // we better go get the flag + return SuspendFor( new CTFBotFetchFlag( TEMPORARY_FLAG_FETCH ), "Fetching lost flag..." ); + } + } + } + } + } + + return Continue(); +} + diff --git a/game/server/tf/bot/behavior/tf_bot_scenario_monitor.h b/game/server/tf/bot/behavior/tf_bot_scenario_monitor.h new file mode 100644 index 0000000..e0e4f89 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_scenario_monitor.h @@ -0,0 +1,27 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_scenario_monitor.h +// Behavior layer that interrupts for scenario rules (picked up flag, drop what you're doing and capture, etc) +// Michael Booth, May 2011 + +#ifndef TF_BOT_SCENARIO_MONITOR_H +#define TF_BOT_SCENARIO_MONITOR_H + +class CTFBotScenarioMonitor : public Action< CTFBot > +{ +public: + virtual Action< CTFBot > *InitialContainedAction( CTFBot *me ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "ScenarioMonitor"; } + +private: + CountdownTimer m_ignoreLostFlagTimer; + CountdownTimer m_lostFlagTimer; + + virtual Action< CTFBot > *DesiredScenarioAndClassAction( CTFBot *me ); +}; + + +#endif // TF_BOT_SCENARIO_MONITOR_H diff --git a/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.cpp b/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.cpp new file mode 100644 index 0000000..5e78ea3 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.cpp @@ -0,0 +1,250 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_seek_and_destroy.h +// Roam the environment, attacking victims +// Michael Booth, January 2010 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "team_control_point_master.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_attack.h" +#include "bot/behavior/tf_bot_seek_and_destroy.h" +#include "bot/behavior/sniper/tf_bot_sniper_attack.h" +#include "nav_mesh.h" + +extern ConVar tf_bot_path_lookahead_range; +extern ConVar tf_bot_offense_must_push_time; +extern ConVar tf_bot_defense_must_defend_time; + +ConVar tf_bot_debug_seek_and_destroy( "tf_bot_debug_seek_and_destroy", "0", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +CTFBotSeekAndDestroy::CTFBotSeekAndDestroy( float duration ) +{ + if ( duration > 0.0f ) + { + m_giveUpTimer.Start( duration ); + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSeekAndDestroy::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + RecomputeSeekPath( me ); + + CTeamControlPoint *point = me->GetMyControlPoint(); + m_isPointLocked = ( point && point->IsLocked() ); + + // restart the timer if we have one + if ( m_giveUpTimer.HasStarted() ) + { + m_giveUpTimer.Reset(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSeekAndDestroy::Update( CTFBot *me, float interval ) +{ + if ( m_giveUpTimer.HasStarted() && m_giveUpTimer.IsElapsed() ) + { + return Done( "Behavior duration elapsed" ); + } + + if ( TFGameRules()->IsInTraining() ) + { + // if the trainee has started capturing the point, assist them + if ( me->IsAnyPointBeingCaptured() ) + { + return Done( "Assist trainee in capturing the point" ); + } + } + else + { + if ( me->IsCapturingPoint() ) + { + return Done( "Keep capturing point I happened to stumble upon" ); + } + + if ( m_isPointLocked ) + { + CTeamControlPoint *point = me->GetMyControlPoint(); + + if ( point && !point->IsLocked() ) + { + return Done( "The point just unlocked" ); + } + } + + if ( !TFGameRules()->RoundHasBeenWon() && me->GetTimeLeftToCapture() < tf_bot_offense_must_push_time.GetFloat() ) + { + return Done( "Time to push for the objective" ); + } + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat ) + { + if ( TFGameRules()->RoundHasBeenWon() ) + { + // hunt down the losers + return SuspendFor( new CTFBotAttack, "Chasing down the losers" ); + } + + const float engageRange = 1000.0f; + if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), engageRange ) ) + { + return SuspendFor( new CTFBotAttack, "Going after an enemy" ); + } + } + + // move towards our seek goal + m_path.Update( me ); + + if ( !m_path.IsValid() && m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( 1.0f ); + + RecomputeSeekPath( me ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotSeekAndDestroy::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ) +{ + RecomputeSeekPath( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnStuck( CTFBot *me ) +{ + RecomputeSeekPath( me ); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnMoveToSuccess( CTFBot *me, const Path *path ) +{ + RecomputeSeekPath( me ); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ) +{ + RecomputeSeekPath( me ); + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSeekAndDestroy::ShouldRetreat( const INextBot *meBot ) const +{ + CTFBot *me = (CTFBot *)meBot->GetEntity(); + + if ( me->IsPlayerClass( TF_CLASS_PYRO ) ) + { + return ANSWER_NO; + } + + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +QueryResultType CTFBotSeekAndDestroy::ShouldHurry( const INextBot *me ) const +{ + return ANSWER_UNDEFINED; +} + + +//--------------------------------------------------------------------------------------------- +CTFNavArea *CTFBotSeekAndDestroy::ChooseGoalArea( CTFBot *me ) +{ + CUtlVector< CTFNavArea * > goalVector; + + TheTFNavMesh()->CollectSpawnRoomThresholdAreas( &goalVector, GetEnemyTeam( me->GetTeamNumber() ) ); + + CTeamControlPoint *point = me->GetMyControlPoint(); + if ( point && !point->IsLocked() ) + { + // add current control point as a seek goal + const CUtlVector< CTFNavArea * > *controlPointAreas = TheTFNavMesh()->GetControlPointAreas( point->GetPointIndex() ); + if ( controlPointAreas && controlPointAreas->Count() > 0 ) + { + goalVector.AddToTail( controlPointAreas->Element( RandomInt( 0, controlPointAreas->Count()-1 ) ) ); + } + } + + if ( tf_bot_debug_seek_and_destroy.GetBool() ) + { + for( int i=0; i<goalVector.Count(); ++i ) + { + TheNavMesh->AddToSelectedSet( goalVector[i] ); + } + } + + // pick a new goal + if ( goalVector.Count() > 0 ) + { + return goalVector[ RandomInt( 0, goalVector.Count()-1 ) ]; + } + + return NULL; +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotSeekAndDestroy::RecomputeSeekPath( CTFBot *me ) +{ + m_goalArea = ChooseGoalArea( me ); + if ( m_goalArea ) + { + CTFBotPathCost cost( me, SAFEST_ROUTE ); + m_path.Compute( me, m_goalArea->GetCenter(), cost ); + } + else + { + m_path.Invalidate(); + } +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnTerritoryContested( CTFBot *me, int territoryID ) +{ + return TryDone( RESULT_IMPORTANT, "Defending the point" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnTerritoryCaptured( CTFBot *me, int territoryID ) +{ + return TryDone( RESULT_IMPORTANT, "Giving up due to point capture" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotSeekAndDestroy::OnTerritoryLost( CTFBot *me, int territoryID ) +{ + return TryDone( RESULT_IMPORTANT, "Giving up due to point lost" ); +} + diff --git a/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.h b/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.h new file mode 100644 index 0000000..26dba48 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_seek_and_destroy.h @@ -0,0 +1,51 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_seek_and_destroy.h +// Roam the environment, attacking victims +// Michael Booth, January 2010 + +#ifndef TF_BOT_SEEK_AND_DESTROY_H +#define TF_BOT_SEEK_AND_DESTROY_H + +#include "Path/NextBotChasePath.h" + + +// +// Roam around the map attacking enemies +// +class CTFBotSeekAndDestroy : public Action< CTFBot > +{ +public: + CTFBotSeekAndDestroy( float duration = -1.0f ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual ActionResult< CTFBot > OnResume( CTFBot *me, Action< CTFBot > *interruptingAction ); + + virtual EventDesiredResult< CTFBot > OnStuck( CTFBot *me ); + virtual EventDesiredResult< CTFBot > OnMoveToSuccess( CTFBot *me, const Path *path ); + virtual EventDesiredResult< CTFBot > OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason ); + + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + + virtual EventDesiredResult< CTFBot > OnTerritoryCaptured( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryLost( CTFBot *me, int territoryID ); + virtual EventDesiredResult< CTFBot > OnTerritoryContested( CTFBot *me, int territoryID ); + + virtual const char *GetName( void ) const { return "SeekAndDestroy"; }; + +private: + CTFNavArea *m_goalArea; + CTFNavArea *ChooseGoalArea( CTFBot *me ); + bool m_isPointLocked; + + PathFollower m_path; + CountdownTimer m_repathTimer; + void RecomputeSeekPath( CTFBot *me ); + + CountdownTimer m_giveUpTimer; +}; + + +#endif // TF_BOT_SEEK_AND_DESTROY_H diff --git a/game/server/tf/bot/behavior/tf_bot_tactical_monitor.cpp b/game/server/tf/bot/behavior/tf_bot_tactical_monitor.cpp new file mode 100644 index 0000000..e68cd9a --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_tactical_monitor.cpp @@ -0,0 +1,659 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_tactical_monitor.cpp +// Behavior layer that interrupts for ammo/health/retreat/etc +// Michael Booth, June 2009 + +#include "cbase.h" +#include "fmtstr.h" + +#include "tf_gamerules.h" +#include "tf_weapon_pipebomblauncher.h" +#include "NextBot/NavMeshEntities/func_nav_prerequisite.h" + +#include "bot/tf_bot.h" +#include "bot/tf_bot_manager.h" + +#include "bot/behavior/tf_bot_tactical_monitor.h" +#include "bot/behavior/tf_bot_scenario_monitor.h" + +#include "bot/behavior/tf_bot_seek_and_destroy.h" +#include "bot/behavior/tf_bot_retreat_to_cover.h" +#include "bot/behavior/tf_bot_taunt.h" +#include "bot/behavior/tf_bot_get_health.h" +#include "bot/behavior/tf_bot_get_ammo.h" +#include "bot/behavior/tf_bot_destroy_enemy_sentry.h" +#include "bot/behavior/tf_bot_use_teleporter.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/engineer/tf_bot_engineer_building.h" +#include "bot/behavior/squad/tf_bot_escort_squad_leader.h" + +#include "bot/behavior/training/tf_bot_training.h" +#include "bot/map_entities/tf_bot_hint_sentrygun.h" + +#include "tf_obj_sentrygun.h" +#include "tf_item_system.h" + +extern ConVar tf_bot_health_ok_ratio; +extern ConVar tf_bot_health_critical_ratio; + +ConVar tf_bot_force_jump( "tf_bot_force_jump", "0", FCVAR_CHEAT, "Force bots to continuously jump" ); + + +Action< CTFBot > *CTFBotTacticalMonitor::InitialContainedAction( CTFBot *me ) +{ + return new CTFBotScenarioMonitor; +} + + +//----------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotTacticalMonitor::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + return Continue(); +} + + +//----------------------------------------------------------------------------------------- +void CTFBotTacticalMonitor::MonitorArmedStickyBombs( CTFBot *me ) +{ + if ( m_stickyBombCheckTimer.IsElapsed() ) + { + m_stickyBombCheckTimer.Start( RandomFloat( 0.3f, 1.0f ) ); + + // are there any enemies on/near my sticky bombs? + CTFPipebombLauncher *gun = dynamic_cast< CTFPipebombLauncher * >( me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); + if ( gun ) + { + const CUtlVector< CHandle< CTFGrenadePipebombProjectile > > &pipeBombVector = gun->GetPipeBombVector(); + + if ( pipeBombVector.Count() > 0 ) + { + CUtlVector< CKnownEntity > knownVector; + me->GetVisionInterface()->CollectKnownEntities( &knownVector ); + + for( int p=0; p<pipeBombVector.Count(); ++p ) + { + CTFGrenadePipebombProjectile *sticky = pipeBombVector[p]; + if ( !sticky ) + { + continue; + } + + for( int k=0; k<knownVector.Count(); ++k ) + { + if ( knownVector[k].IsObsolete() ) + { + continue; + } + + if ( knownVector[k].GetEntity()->IsBaseObject() ) + { + // we want to put several stickies on a sentry and det at once + continue; + } + + if ( sticky->GetTeamNumber() != GetEnemyTeam( knownVector[k].GetEntity()->GetTeamNumber() ) ) + { + // "known" is either a spectator, or on our team + continue; + } + + const float closeRange = 150.0f; + if ( ( knownVector[k].GetLastKnownPosition() - sticky->GetAbsOrigin() ).IsLengthLessThan( closeRange ) ) + { + // they are close - blow it! + me->PressAltFireButton(); + return; + } + } + } + } + } + } +} + + +//----------------------------------------------------------------------------------------- +void CTFBotTacticalMonitor::AvoidBumpingEnemies( CTFBot *me ) +{ + if ( me->GetDifficulty() < CTFBot::HARD ) + return; + + const float avoidRange = 200.0f; + + CUtlVector< CTFPlayer * > enemyVector; + CollectPlayers( &enemyVector, GetEnemyTeam( me->GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS ); + + CTFPlayer *closestEnemy = NULL; + float closestRangeSq = avoidRange * avoidRange; + + for( int i=0; i<enemyVector.Count(); ++i ) + { + CTFPlayer *enemy = enemyVector[i]; + + if ( enemy->m_Shared.IsStealthed() || enemy->m_Shared.InCond( TF_COND_DISGUISED ) ) + continue; + + float rangeSq = ( enemy->GetAbsOrigin() - me->GetAbsOrigin() ).LengthSqr(); + if ( rangeSq < closestRangeSq ) + { + closestEnemy = enemy; + closestRangeSq = rangeSq; + } + } + + if ( !closestEnemy ) + return; + + // avoid unless hindrance returns a definitive "no" + if ( me->GetIntentionInterface()->IsHindrance( me, closestEnemy ) == ANSWER_UNDEFINED ) + { + me->ReleaseForwardButton(); + me->ReleaseLeftButton(); + me->ReleaseRightButton(); + me->ReleaseBackwardButton(); + + Vector away = me->GetAbsOrigin() - closestEnemy->GetAbsOrigin(); + + me->GetLocomotionInterface()->Approach( me->GetLocomotionInterface()->GetFeet() + away ); + } +} + + +//----------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotTacticalMonitor::Update( CTFBot *me, float interval ) +{ + if ( TFGameRules()->RoundHasBeenWon() ) + { +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsBossBattleMode() ) + { + return Continue(); + } +#endif // TF_RAID_MODE + if ( TFGameRules()->GetWinningTeam() == me->GetTeamNumber() ) + { + // we won - kill all losers we see + return SuspendFor( new CTFBotSeekAndDestroy, "Get the losers!" ); + } + else + { + // lost - run and hide + if ( me->GetVisionInterface()->GetPrimaryKnownThreat( true ) ) + { + return SuspendFor( new CTFBotRetreatToCover, "Run away from threat!" ); + } + + me->PressCrouchButton(); + } + + return Continue(); + } + + if ( tf_bot_force_jump.GetBool() ) + { + if ( !me->GetLocomotionInterface()->IsClimbingOrJumping() ) + { + me->GetLocomotionInterface()->Jump(); + } + } + + if ( TFGameRules()->State_Get() == GR_STATE_PREROUND ) + { + // clear stuck monitor so we dont jump when the preround elapses + me->GetLocomotionInterface()->ClearStuckStatus( "In preround" ); + } + + Action< CTFBot > *result = me->OpportunisticallyUseWeaponAbilities(); + if ( result ) + { + return SuspendFor( result, "Opportunistically using buff item" ); + } + + if ( TFGameRules()->InSetup() ) + { + // if a human is staring at us, face them and taunt + if ( m_acknowledgeRetryTimer.IsElapsed() ) + { + CTFPlayer *watcher = me->GetClosestHumanLookingAtMe(); + if ( watcher ) + { + if ( !m_attentionTimer.HasStarted() ) + m_attentionTimer.Start( 0.5f ); + + if ( m_attentionTimer.HasStarted() && m_attentionTimer.IsElapsed() ) + { + // a human has been staring at us - acknowledge them + if ( !m_acknowledgeAttentionTimer.HasStarted() ) + { + // look toward them + me->GetBodyInterface()->AimHeadTowards( watcher, IBody::IMPORTANT, 3.0f, NULL, "Acknowledging friendly human attention" ); + m_acknowledgeAttentionTimer.Start( RandomFloat( 0.0f, 2.0f ) ); + } + else if ( m_acknowledgeAttentionTimer.IsElapsed() ) + { + m_acknowledgeAttentionTimer.Invalidate(); + + // don't ack again for awhile + m_acknowledgeRetryTimer.Start( RandomFloat( 10.0f, 20.0f ) ); + + return SuspendFor( new CTFBotTaunt, "Acknowledging friendly human attention" ); + } + } + } + else + { + // no-one is looking at me + m_attentionTimer.Invalidate(); + } + } + } + + // check if we need to get to cover + QueryResultType shouldRetreat = me->GetIntentionInterface()->ShouldRetreat( me ); + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + // never retreat in MvM mode + shouldRetreat = ANSWER_NO; + } + + if ( shouldRetreat == ANSWER_YES ) + { + return SuspendFor( new CTFBotRetreatToCover, "Backing off" ); + } + else if ( shouldRetreat != ANSWER_NO ) + { + // retreat if we need to do a full reload (ie: soldiers shot all their rockets) + if ( !me->m_Shared.InCond( TF_COND_INVULNERABLE ) ) + { + if ( me->IsDifficulty( CTFBot::HARD ) || me->IsDifficulty( CTFBot::EXPERT ) ) + { + CTFWeaponBase *myPrimary = (CTFWeaponBase *)me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ); + if ( myPrimary && me->GetAmmoCount( TF_AMMO_PRIMARY ) > 0 && me->IsBarrageAndReloadWeapon( myPrimary ) ) + { + if ( myPrimary->Clip1() <= 1 ) + { + return SuspendFor( new CTFBotRetreatToCover, "Moving to cover to reload" ); + } + } + } + } + } + + bool isAvailable = ( me->GetIntentionInterface()->ShouldHurry( me ) != ANSWER_YES ); + + if ( TFGameRules()->IsMannVsMachineMode() && me->HasTheFlag() ) + { + isAvailable = false; + } + + // collect ammo and health kits, unless we're in a big hurry + if ( isAvailable && m_maintainTimer.IsElapsed() ) + { + m_maintainTimer.Start( RandomFloat( 0.3f, 0.5f ) ); + + bool isHurt = false; + + if ( me->IsInCombat() || me->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + // stay in the fight until we're nearly dead + isHurt = ( (float)me->GetHealth() / (float)me->GetMaxHealth() ) < tf_bot_health_critical_ratio.GetFloat(); + } + else + { + isHurt = me->m_Shared.InCond( TF_COND_BURNING ) || ( (float)me->GetHealth() / (float)me->GetMaxHealth() ) < tf_bot_health_ok_ratio.GetFloat(); + } + + if ( isHurt && CTFBotGetHealth::IsPossible( me ) ) + { + return SuspendFor( new CTFBotGetHealth, "Grabbing nearby health" ); + } + + if ( me->IsAmmoLow() && CTFBotGetAmmo::IsPossible( me ) ) + { + return SuspendFor( new CTFBotGetAmmo, "Grabbing nearby ammo" ); + } + + bool shouldDestroySentries = true; + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + shouldDestroySentries = false; + } + + // destroy enemy sentry guns we've encountered + if ( shouldDestroySentries && me->GetEnemySentry() && CTFBotDestroyEnemySentry::IsPossible( me ) ) + { + return SuspendFor( new CTFBotDestroyEnemySentry, "Going after an enemy sentry to destroy it" ); + } + } + + // opportunistically use nearby teleporters + if ( ShouldOpportunisticallyTeleport( me ) ) + { + CObjectTeleporter *nearbyTeleporter = FindNearbyTeleporter( me ); + if ( nearbyTeleporter ) + { + CTFNavArea *teleporterArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( nearbyTeleporter ); + CTFNavArea *myArea = (CTFNavArea *)me->GetLastKnownArea(); + + // only use teleporter if it is ahead of us + if ( teleporterArea && myArea && myArea->GetIncursionDistance( me->GetTeamNumber() ) < 350.0f + teleporterArea->GetIncursionDistance( me->GetTeamNumber() ) ) + { + return SuspendFor( new CTFBotUseTeleporter( nearbyTeleporter ), "Using nearby teleporter" ); + } + } + } + + // detonate sticky bomb traps when victims are near + MonitorArmedStickyBombs( me ); + + // if we're a Spy, avoid bumping into enemies and giving ourselves away + if ( me->IsPlayerClass( TF_CLASS_SPY ) ) + { + AvoidBumpingEnemies( me ); + } + + me->UpdateDelayedThreatNotices(); + + // if I'm a squad leader, wait for out of position squadmates + if ( me->IsInASquad() && me->GetSquad()->IsLeader( me ) && me->GetSquad()->ShouldSquadLeaderWaitForFormation() ) + { + return SuspendFor( new CTFBotWaitForOutOfPositionSquadMember, "Waiting for squadmates to get back into formation" ); + } + + + return Continue(); +} + + +//----------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotTacticalMonitor::OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) +{ + return TryContinue(); +} + + +//----------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotTacticalMonitor::OnNavAreaChanged( CTFBot *me, CNavArea *newArea, CNavArea *oldArea ) +{ + // does the area we are entering have a prerequisite? + if ( newArea && newArea->HasPrerequisite( me ) && !me->HasAttribute( CTFBot::AGGRESSIVE ) ) + { + const CUtlVector< CHandle< CFuncNavPrerequisite > > &prereqVector = newArea->GetPrerequisiteVector(); + + for( int i=0; i<prereqVector.Count(); ++i ) + { + const CFuncNavPrerequisite *prereq = prereqVector[i]; + if ( prereq && prereq->IsEnabled() && const_cast< CFuncNavPrerequisite * >( prereq )->PassesTriggerFilters( me ) ) + { + // this prerequisite applies to me + if ( prereq->IsTask( CFuncNavPrerequisite::TASK_WAIT ) ) + { + return TrySuspendFor( new CTFBotNavEntWait( prereq ), RESULT_IMPORTANT, "Prerequisite commands me to wait" ); + } + else if ( prereq->IsTask( CFuncNavPrerequisite::TASK_MOVE_TO_ENTITY ) ) + { + return TrySuspendFor( new CTFBotNavEntMoveTo( prereq ), RESULT_IMPORTANT, "Prerequisite commands me to move to an entity" ); + } + } + } + } + + + return TryContinue(); +} + +//----------------------------------------------------------------------------------------- +EventDesiredResult< CTFBot > CTFBotTacticalMonitor::OnCommandString( CTFBot *me, const char *command ) +{ + if ( FStrEq( command, "goto action point" ) ) + { + return TrySuspendFor( new CTFGotoActionPoint(), RESULT_IMPORTANT, "Received command to go to action point" ); + } + else if ( FStrEq( command, "despawn" ) ) + { + return TrySuspendFor( new CTFDespawn(), RESULT_CRITICAL, "Received command to go to de-spawn" ); + } + else if ( FStrEq( command, "taunt" ) ) + { + return TrySuspendFor( new CTFBotTaunt(), RESULT_TRY, "Received command to taunt" ); + } + else if ( FStrEq( command, "cloak" ) ) + { + if ( me->IsPlayerClass( TF_CLASS_SPY ) && me->m_Shared.IsStealthed() == false ) + { + me->PressAltFireButton(); + } + } + else if ( FStrEq( command, "uncloak" ) ) + { + if ( me->IsPlayerClass( TF_CLASS_SPY ) && me->m_Shared.IsStealthed() == true ) + { + me->PressAltFireButton(); + } + } + else if ( FStrEq( command, "disguise") ) + { + if ( me->IsPlayerClass( TF_CLASS_SPY ) ) + { + if ( me->CanDisguise() ) + { + me->m_Shared.Disguise( GetEnemyTeam( me->GetTeamNumber() ), RandomInt( TF_FIRST_NORMAL_CLASS, TF_LAST_NORMAL_CLASS-1 ) ); + } + } + } + else if ( FStrEq( command, "build sentry at nearest sentry hint" ) ) + { + if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + CTFBotHintSentrygun *bestSentryHint = NULL; + float bestDist2 = FLT_MAX; + CTFBotHintSentrygun *sentryHint; + for( sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( NULL, "bot_hint_sentrygun" ) ); + sentryHint; + sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( sentryHint, "bot_hint_sentrygun" ) ) ) + { + // clear the previous owner if it is us + if ( sentryHint->GetPlayerOwner() == me ) + { + sentryHint->SetPlayerOwner( NULL ); + } + if ( sentryHint->IsAvailableForSelection( me ) ) + { + Vector toMe = me->GetAbsOrigin() - sentryHint->GetAbsOrigin(); + float dist2 = toMe.LengthSqr(); + if ( dist2 < bestDist2 ) + { + bestSentryHint = sentryHint; + bestDist2 = dist2; + } + } + } + if ( bestSentryHint ) + { + bestSentryHint->SetPlayerOwner( me ); + return TrySuspendFor( new CTFBotEngineerBuilding( bestSentryHint ), RESULT_CRITICAL, "Building a Sentry at a hint location" ); + } + } + } + else if ( FStrEq( command, "attack sentry at next action point" ) ) + { + return TrySuspendFor( new CTFTrainingAttackSentryActionPoint(), RESULT_CRITICAL, "Received command to attack sentry gun at next action point" ); + } +#ifdef STAGING_ONLY + // !!! BountyMode prototype evaluation hacks below - this code will most likely be deleted soon + else if ( FStrEq( command, "become raider" ) ) + { + me->SetIsMiniBoss( true ); + me->SetScaleOverride( 1.75f ); + me->ModifyMaxHealth( 5000 ); + me->SetWeaponRestriction( CTFBot::PRIMARY_ONLY ); + me->GetPlayerClass()->SetCustomModel( g_szBotBossModels[ me->GetPlayerClass()->GetClassIndex() ], USE_CLASS_ANIMATIONS ); + me->UpdateModel(); + me->SetBloodColor( DONT_BLEED ); + engine->SetFakeClientConVarValue( me->edict(), "name", "Raider" ); + + // Custom attribs + struct botAttribs_t + { + char szName[MAX_ATTRIBUTE_DESCRIPTION_LENGTH]; + float flValue; + }; + + botAttribs_t sAttribs[] = + { + { "move speed bonus", 0.5f }, + { "damage bonus", 1.5f }, + { "damage force reduction", 0.3f }, + { "airblast vulnerability multiplier", 0.3f }, + { "override footstep sound set", 2.f }, + }; + + CAttributeList *pAttribList = me->GetAttributeList(); + if ( pAttribList ) + { + for ( int i = 0; i < ARRAYSIZE( sAttribs ); i++ ) + { + const CEconItemAttributeDefinition *pDef = ItemSystem()->GetItemSchema()->GetAttributeDefinitionByName( sAttribs[i].szName ); + if ( pDef ) + { + pAttribList->SetRuntimeAttributeValue( pDef, sAttribs[i].flValue ); + } + } + me->NetworkStateChanged(); + } + } + // !!! BountyMode prototype evaluation hacks below - this code will most likely be deleted soon + else if ( FStrEq( command, "become guardian" ) ) + { + me->SetIsMiniBoss( true ); + me->SetScaleOverride( 1.75f ); + me->ModifyMaxHealth( 3300 ); + me->SetWeaponRestriction( CTFBot::PRIMARY_ONLY ); + me->GetPlayerClass()->SetCustomModel( g_szBotBossModels[ me->GetPlayerClass()->GetClassIndex() ], USE_CLASS_ANIMATIONS ); + me->UpdateModel(); + me->SetBloodColor( DONT_BLEED ); + engine->SetFakeClientConVarValue( me->edict(), "name", "Guardian" ); + me->SetAttribute( CTFBot::PRIORITIZE_DEFENSE ); + + // Custom attribs + struct botAttribs_t + { + char szName[MAX_ATTRIBUTE_DESCRIPTION_LENGTH]; + float flValue; + }; + + botAttribs_t sAttribs[] = + { + { "move speed bonus", 0.5f }, + { "faster reload rate", -0.4f }, + { "fire rate bonus", 0.75f }, + { "damage force reduction", 0.5f }, + { "airblast vulnerability multiplier", 0.5f }, + { "override footstep sound set", 4.f }, + }; + + CAttributeList *pAttribList = me->GetAttributeList(); + if ( pAttribList ) + { + for ( int i = 0; i < ARRAYSIZE( sAttribs ); i++ ) + { + const CEconItemAttributeDefinition *pDef = ItemSystem()->GetItemSchema()->GetAttributeDefinitionByName( sAttribs[i].szName ); + if ( pDef ) + { + pAttribList->SetRuntimeAttributeValue( pDef, sAttribs[i].flValue ); + } + } + me->NetworkStateChanged(); + } + } +#endif // STAGING_ONLY + + return TryContinue(); +} + + +//----------------------------------------------------------------------------------------- +bool CTFBotTacticalMonitor::ShouldOpportunisticallyTeleport( CTFBot *me ) const +{ + // if I'm an engineer who hasn't placed his teleport entrance yet, don't use friend's teleporter + if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + CBaseObject *teleporterEntrance = me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_ENTRANCE ); + + return ( teleporterEntrance != NULL ); + } + + // Medics don't automatically take teleporters unless they actively decide to follow their patient through + if ( me->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + return false; + } + + return true; +} + + +//----------------------------------------------------------------------------------------- +CObjectTeleporter *CTFBotTacticalMonitor::FindNearbyTeleporter( CTFBot *me ) +{ + if ( !m_findTeleporterTimer.IsElapsed() ) + { + return NULL; + } + + m_findTeleporterTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFNavArea *myArea = (CTFNavArea *)me->GetLastKnownArea(); + if ( myArea == NULL ) + { + return NULL; + } + + CUtlVector< CNavArea * > nearbyAreaVector; + CUtlVector< CBaseObject * > objVector; + CUtlVector< CObjectTeleporter * > nearbyTeleporterEntranceVector; + + CollectSurroundingAreas( &nearbyAreaVector, myArea, 1000.0f ); + TheTFNavMesh()->CollectBuiltObjects( &objVector, me->GetTeamNumber() ); + + for( int j=0; j<objVector.Count(); ++j ) + { + if ( objVector[j]->GetType() == OBJ_TELEPORTER ) + { + CObjectTeleporter *teleporter = (CObjectTeleporter *)objVector[j]; + + teleporter->UpdateLastKnownArea(); + + CNavArea *teleporterArea = teleporter->GetLastKnownArea(); + + if ( teleporter->IsEntrance() && teleporter->IsReady() && teleporterArea ) + { + // we've found a functional teleporter entrance - is it in our nearby area set? + for( int i=0; i<nearbyAreaVector.Count(); ++i ) + { + CNavArea *nearbyArea = nearbyAreaVector[i]; + + if ( nearbyArea->GetID() == teleporterArea->GetID() ) + { + // yes, it's nearby + nearbyTeleporterEntranceVector.AddToTail( teleporter ); + break; + } + } + } + } + } + + if ( nearbyTeleporterEntranceVector.Count() > 0 ) + { + int which = RandomInt( 0, nearbyTeleporterEntranceVector.Count()-1 ); + + return nearbyTeleporterEntranceVector[ which ]; + } + + return NULL; +} diff --git a/game/server/tf/bot/behavior/tf_bot_tactical_monitor.h b/game/server/tf/bot/behavior/tf_bot_tactical_monitor.h new file mode 100644 index 0000000..1bbd658 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_tactical_monitor.h @@ -0,0 +1,47 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_tactical_monitor.h +// Behavior layer that interrupts for ammo/health/retreat/etc +// Michael Booth, June 2009 + +#ifndef TF_BOT_TACTICAL_MONITOR_H +#define TF_BOT_TACTICAL_MONITOR_H + +class CObjectTeleporter; + +class CTFBotTacticalMonitor : public Action< CTFBot > +{ +public: + virtual Action< CTFBot > *InitialContainedAction( CTFBot *me ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual EventDesiredResult< CTFBot > OnNavAreaChanged( CTFBot *me, CNavArea *newArea, CNavArea *oldArea ); + virtual EventDesiredResult< CTFBot > OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ); + + // @note Tom Bui: Currently used for the training stuff, but once we get that interface down, we will turn that + // into a proper API + virtual EventDesiredResult< CTFBot > OnCommandString( CTFBot *me, const char *command ); + + virtual const char *GetName( void ) const { return "TacticalMonitor"; } + +private: + CountdownTimer m_maintainTimer; + + CountdownTimer m_acknowledgeAttentionTimer; + CountdownTimer m_acknowledgeRetryTimer; + CountdownTimer m_attentionTimer; + + CountdownTimer m_stickyBombCheckTimer; + void MonitorArmedStickyBombs( CTFBot *me ); + + bool ShouldOpportunisticallyTeleport( CTFBot *me ) const; + CObjectTeleporter *FindNearbyTeleporter( CTFBot *me ); + CountdownTimer m_findTeleporterTimer; + + void AvoidBumpingEnemies( CTFBot *me ); +}; + + + +#endif // TF_BOT_TACTICAL_MONITOR_H diff --git a/game/server/tf/bot/behavior/tf_bot_taunt.cpp b/game/server/tf/bot/behavior/tf_bot_taunt.cpp new file mode 100644 index 0000000..b0034fa --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_taunt.cpp @@ -0,0 +1,53 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_taunt.cpp +// Stand still and play a taunt animation +// Michael Booth, November 2009 + +#include "cbase.h" +#include "team.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_taunt.h" + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotTaunt::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + // wait a short random time so entire mob doesn't taunt in unison + m_tauntTimer.Start( RandomFloat( 0, 1.0f ) ); + m_didTaunt = false; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotTaunt::Update( CTFBot *me, float interval ) +{ + if ( m_tauntTimer.IsElapsed() ) + { + if ( m_didTaunt ) + { + // Stop taunting after a while + if ( m_tauntEndTimer.IsElapsed() && me->m_Shared.GetTauntIndex() == TAUNT_LONG ) + { + me->EndLongTaunt(); + } + + if ( me->m_Shared.InCond( TF_COND_TAUNTING ) == false ) + { + return Done( "Taunt finished" ); + } + } + else + { + me->HandleTauntCommand(); + // Start a timer to end our taunt in case we're still going after awhile + m_tauntEndTimer.Start( RandomFloat( 3.f, 5.f ) ); + + m_didTaunt = true; + } + } + + return Continue(); +} + diff --git a/game/server/tf/bot/behavior/tf_bot_taunt.h b/game/server/tf/bot/behavior/tf_bot_taunt.h new file mode 100644 index 0000000..37a9074 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_taunt.h @@ -0,0 +1,26 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_taunt.h +// Stand still and play a taunt animation +// Michael Booth, November 2009 + +#ifndef TF_BOT_TAUNT_H +#define TF_BOT_TAUNT_H + + +//----------------------------------------------------------------------------- +class CTFBotTaunt : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "Taunt"; }; + +private: + CountdownTimer m_tauntTimer; + CountdownTimer m_tauntEndTimer; + bool m_didTaunt; +}; + + +#endif // TF_BOT_TAUNT_H diff --git a/game/server/tf/bot/behavior/tf_bot_use_item.cpp b/game/server/tf/bot/behavior/tf_bot_use_item.cpp new file mode 100644 index 0000000..ee8e9ec --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_use_item.cpp @@ -0,0 +1,73 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_use_item.h +// Equip and consume an item +// Michael Booth, July 2011 + +#include "cbase.h" +#include "tf_weaponbase.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_use_item.h" + + +//--------------------------------------------------------------------------------------------- +CTFBotUseItem::CTFBotUseItem( CTFWeaponBase *item ) +{ + m_item = item; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotUseItem::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + // force-equip the item we're going to use + me->PushRequiredWeapon( m_item ); + + m_cooldownTimer.Start( m_item->m_flNextPrimaryAttack - gpGlobals->curtime + 0.25f ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotUseItem::Update( CTFBot *me, float interval ) +{ + if ( m_item == NULL ) + { + return Done( "NULL item" ); + } + + CTFWeaponBase *myCurrentWeapon = me->m_Shared.GetActiveTFWeapon(); + + if ( !myCurrentWeapon ) + { + return Done( "NULL weapon" ); + } + + if ( m_cooldownTimer.HasStarted() ) + { + if ( m_cooldownTimer.IsElapsed() ) + { + // use it + me->PressFireButton(); + m_cooldownTimer.Invalidate(); + } + } + else // used + { + // some items use the taunt system - wait for the taunt to end + if ( !me->IsTaunting() ) + { + return Done( "Item used" ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBotUseItem::OnEnd( CTFBot *me, Action< CTFBot > *nextAction ) +{ + me->PopRequiredWeapon(); +} + diff --git a/game/server/tf/bot/behavior/tf_bot_use_item.h b/game/server/tf/bot/behavior/tf_bot_use_item.h new file mode 100644 index 0000000..33467a3 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_use_item.h @@ -0,0 +1,27 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_use_item.h +// Equip and consume an item +// Michael Booth, July 2011 + +#ifndef TF_BOT_USE_ITEM_H +#define TF_BOT_USE_ITEM_H + +class CTFBotUseItem : public Action< CTFBot > +{ +public: + CTFBotUseItem( CTFWeaponBase *item ); + virtual ~CTFBotUseItem() { } + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual void OnEnd( CTFBot *me, Action< CTFBot > *nextAction ); + + virtual const char *GetName( void ) const { return "UseItem"; }; + +private: + CHandle< CTFWeaponBase > m_item; + CountdownTimer m_cooldownTimer; +}; + + +#endif // TF_BOT_USE_ITEM_H diff --git a/game/server/tf/bot/behavior/tf_bot_use_teleporter.cpp b/game/server/tf/bot/behavior/tf_bot_use_teleporter.cpp new file mode 100644 index 0000000..fc249fb --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_use_teleporter.cpp @@ -0,0 +1,123 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_use_teleporter.cpp +// Ride a friendly teleporter +// Michael Booth, May 2010 + +#include "cbase.h" +#include "nav_mesh.h" +#include "tf_player.h" +#include "bot/tf_bot.h" +#include "bot/behavior/tf_bot_use_teleporter.h" + +extern ConVar tf_bot_path_lookahead_range; + +//--------------------------------------------------------------------------------------------- +CTFBotUseTeleporter::CTFBotUseTeleporter( CObjectTeleporter *teleporter, UseHowType how ) +{ + m_teleporter = teleporter; + m_how = how; + m_isInTransit = false; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotUseTeleporter::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +// We could compute the time it would take to walk from the tele entrance to exit +// and compare that to the time needed to wait for the tele to be ready to send us, +// but players tend to use the tele only if it is ready NOW (or very soon). +bool CTFBotUseTeleporter::IsTeleporterAvailable( void ) const +{ + if ( m_teleporter != NULL ) + { + if ( !m_teleporter->IsReady() ) + return false; + + if ( m_teleporter->GetState() == TELEPORTER_STATE_READY ) + return true; + +/* causes massive bot pileups + if ( m_teleporter->GetState() == TELEPORTER_STATE_SENDING || + m_teleporter->GetState() == TELEPORTER_STATE_RECHARGING ) + { + if ( m_teleporter->GetUpgradeLevel() == 3 ) + { + // we'll wait for level 3 teleporters - they're really fast + return true; + } + } +*/ + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CTFBot > CTFBotUseTeleporter::Update( CTFBot *me, float interval ) +{ + if ( m_teleporter == NULL ) + { + return Done( "Teleporter is gone" ); + } + + CObjectTeleporter *teleporterExit = m_teleporter->GetMatchingTeleporter(); + if ( !teleporterExit ) + { + return Done( "Missing teleporter exit" ); + } + + if ( m_teleporter->IsSendingPlayer( me ) ) + { + // note that we have been teleported, because it takes a few frames + // to actually relocate us, even after our teleporter leaves the "sending" state + m_isInTransit = true; + } + + if ( m_isInTransit ) + { + if ( me->IsRangeLessThan( teleporterExit, 25.0f ) ) + { + return Done( "Successful teleport" ); + } + } + else if ( !IsTeleporterAvailable() && m_how == USE_IF_READY ) + { + return Done( "Teleporter is not available" ); + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleRecently() ) + { + // prepare to fight + me->EquipBestWeaponForThreat( threat ); + } + + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + if ( m_path.Compute( me, m_teleporter->GetAbsOrigin(), cost ) == false ) + { + // no path to teleporter + return Done( "Can't reach teleporter!" ); + } + } + + // move toward the teleporter until we're standing on it + if ( me->GetLocomotionInterface()->GetGround() != m_teleporter ) + { + m_path.Update( me ); + } + + return Continue(); +} + diff --git a/game/server/tf/bot/behavior/tf_bot_use_teleporter.h b/game/server/tf/bot/behavior/tf_bot_use_teleporter.h new file mode 100644 index 0000000..6b36b33 --- /dev/null +++ b/game/server/tf/bot/behavior/tf_bot_use_teleporter.h @@ -0,0 +1,40 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_use_teleporter.h +// Ride a friendly teleporter +// Michael Booth, May 2010 + +#ifndef TF_BOT_USE_TELEPORTER_H +#define TF_BOT_USE_TELEPORTER_H + +#include "tf_obj_teleporter.h" +#include "Path/NextBotPathFollow.h" + +class CTFBotUseTeleporter : public Action< CTFBot > +{ +public: + enum UseHowType + { + USE_IF_READY, + ALWAYS_USE + }; + CTFBotUseTeleporter( CObjectTeleporter *teleporter, UseHowType how = USE_IF_READY ); + + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + + virtual const char *GetName( void ) const { return "UseTeleporter"; }; + +private: + CHandle< CObjectTeleporter > m_teleporter; // the teleporter we're trying to use + UseHowType m_how; + + PathFollower m_path; + CountdownTimer m_repathTimer; + + bool m_isInTransit; + + bool IsTeleporterAvailable( void ) const; +}; + + +#endif // TF_BOT_USE_TELEPORTER_H diff --git a/game/server/tf/bot/behavior/training/tf_bot_training.cpp b/game/server/tf/bot/behavior/training/tf_bot_training.cpp new file mode 100644 index 0000000..7db948b --- /dev/null +++ b/game/server/tf/bot/behavior/training/tf_bot_training.cpp @@ -0,0 +1,133 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +//////////////////////////////////////////////////////////////////////////////////////////////////// +// tf_bot_training.cpp +// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "cbase.h" +#include "team.h" +#include "bot/tf_bot.h" +#include "bot/map_entities/tf_bot_generator.h" +#include "bot/behavior/training/tf_bot_training.h" +#include "tf_obj_sentrygun.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +ActionResult< CTFBot > CTFDespawn::Update( CTFBot *me, float interval ) +{ + // players need to be kicked, not deleted + if ( me->GetEntity()->IsPlayer() ) + { + CBasePlayer *player = dynamic_cast< CBasePlayer * >( me->GetEntity() ); + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", player->GetUserID() ) ); + } + else + { + UTIL_Remove( me->GetEntity() ); + } + return Continue(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +ActionResult< CTFBot > CTFTrainingAttackSentryActionPoint::Update( CTFBot *me, float interval ) +{ + CTFBotActionPoint* pActionPoint = me->GetActionPoint(); + if ( pActionPoint == NULL ) + { + return Done(); + } + + if ( pActionPoint->IsWithinRange( me ) ) + { + CObjectSentrygun *pSentrygun = me->GetEnemySentry(); + if ( pSentrygun ) + { + me->GetBodyInterface()->AimHeadTowards( pSentrygun, IBody::MANDATORY, 1.0f, NULL, "Aiming at enemy sentry" ); + + // because sentries are stationary, check if XY is on target to allow SelectTargetPoint() to adjust Z for grenades + Vector toSentry = pSentrygun->WorldSpaceCenter() - me->EyePosition(); + toSentry.NormalizeInPlace(); + Vector forward; + me->EyeVectors( &forward ); + + if ( ( forward.x * toSentry.x + forward.y * toSentry.y ) > 0.95f ) + { + me->PressFireButton(); + } + } + } + else + { + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, pActionPoint->GetAbsOrigin(), cost ); + } + + m_path.Update( me ); + } + + return Continue(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +ActionResult< CTFBot > CTFGotoActionPoint::OnStart( CTFBot *me, Action< CTFBot > *priorAction ) +{ + m_stayTimer.Invalidate(); + m_wasTeleported = false; + + return Continue(); +} + +ActionResult< CTFBot > CTFGotoActionPoint::Update( CTFBot *me, float interval ) +{ + CTFBotActionPoint* pActionPoint = me->GetActionPoint(); + if ( pActionPoint == NULL ) + { + return Done(); + } + + if ( pActionPoint->IsWithinRange( me ) ) + { + // track if we ever get teleported during this process + m_wasTeleported |= me->m_Shared.InCond( TF_COND_SELECTED_TO_TELEPORT ); + + // we're at the action point + if ( m_stayTimer.HasStarted() == false ) + { + // this method may cause us to become suspended for other actions + pActionPoint->ReachedActionPoint( me ); + + m_stayTimer.Start( pActionPoint->m_stayTime ); + } + else if ( m_stayTimer.IsElapsed() ) + { + me->SetActionPoint( dynamic_cast< CTFBotActionPoint * >( pActionPoint->m_moveGoal.Get() ) ); + return ChangeTo( new CTFGotoActionPoint, "Reached point, going to next" ); + } + } + else if ( m_wasTeleported ) + { + // we reached our action point, but were teleported far away. + // presumably we've resumed, so just go to the next action point. + me->SetActionPoint( dynamic_cast< CTFBotActionPoint * >( pActionPoint->m_moveGoal.Get() ) ); + return ChangeTo( new CTFGotoActionPoint, "Reached point, going to next" ); + } + else + { + if ( m_repathTimer.IsElapsed() ) + { + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFBotPathCost cost( me, FASTEST_ROUTE ); + m_path.Compute( me, pActionPoint->GetAbsOrigin(), cost ); + } + + m_path.Update( me ); + } + return Continue(); +} diff --git a/game/server/tf/bot/behavior/training/tf_bot_training.h b/game/server/tf/bot/behavior/training/tf_bot_training.h new file mode 100644 index 0000000..d8d6cde --- /dev/null +++ b/game/server/tf/bot/behavior/training/tf_bot_training.h @@ -0,0 +1,54 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +//////////////////////////////////////////////////////////////////////////////////////////////////// +// tf_bot_training.h +// +// Misc. training actions/behaviors. To be split up into separate files when we deem them "re-usable" +// +// Tom Bui, April 2010 +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef TF_BOT_TRAINING_H +#define TF_BOT_TRAINING_H + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Attempts to kick/despawn the bot in the Update() + +class CTFDespawn : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual const char *GetName( void ) const { return "Despawn"; }; +}; + + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Simple behavior for training where the bot approaches action point and tries to fire at it (and anything there) + +class CTFTrainingAttackSentryActionPoint : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual const char *GetName( void ) const { return "Despawn"; }; + +private: + CountdownTimer m_repathTimer; + PathFollower m_path; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Tells a bot to go an Action Point and run any command it has +class CTFGotoActionPoint : public Action< CTFBot > +{ +public: + virtual ActionResult< CTFBot > OnStart( CTFBot *me, Action< CTFBot > *priorAction ); + virtual ActionResult< CTFBot > Update( CTFBot *me, float interval ); + virtual const char *GetName( void ) const { return "GotoActionPoint"; }; + +private: + CountdownTimer m_stayTimer; + CountdownTimer m_repathTimer; + PathFollower m_path; + bool m_wasTeleported; +}; + +#endif // TF_BOT_TRAINING_H diff --git a/game/server/tf/bot/map_entities/tf_bot_generator.cpp b/game/server/tf/bot/map_entities/tf_bot_generator.cpp new file mode 100644 index 0000000..38fbc3b --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_generator.cpp @@ -0,0 +1,470 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_generator.cpp +// Entity to spawn a collection of TFBots +// Michael Booth, September 2009 + +#include "cbase.h" + +#include "tf_bot_generator.h" + +#include "bot/tf_bot.h" +#include "bot/tf_bot_manager.h" +#include "tf_gamerules.h" +#include "tier3/tier3.h" +#include "vgui/ILocalize.h" + +extern ConVar tf_bot_prefix_name_with_difficulty; +extern ConVar tf_bot_difficulty; + +extern void CreateBotName( int iTeam, int iClassIndex, CTFBot::DifficultyType skill, char* pBuffer, int iBufferSize ); + +//------------------------------------------------------------------------------ + +BEGIN_DATADESC( CTFBotGenerator ) + DEFINE_KEYFIELD( m_spawnCount, FIELD_INTEGER, "count" ), + DEFINE_KEYFIELD( m_maxActiveCount, FIELD_INTEGER, "maxActive" ), + DEFINE_KEYFIELD( m_spawnInterval, FIELD_FLOAT, "interval" ), + DEFINE_KEYFIELD( m_className, FIELD_STRING, "class" ), + DEFINE_KEYFIELD( m_teamName, FIELD_STRING, "team" ), + DEFINE_KEYFIELD( m_actionPointName, FIELD_STRING, "action_point" ), + DEFINE_KEYFIELD( m_initialCommand, FIELD_STRING, "initial_command" ), + DEFINE_KEYFIELD( m_bSuppressFire, FIELD_BOOLEAN, "suppressFire" ), + DEFINE_KEYFIELD( m_bDisableDodge, FIELD_BOOLEAN, "disableDodge" ), + DEFINE_KEYFIELD( m_iOnDeathAction, FIELD_INTEGER, "actionOnDeath" ), + DEFINE_KEYFIELD( m_bUseTeamSpawnpoint, FIELD_BOOLEAN, "useTeamSpawnPoint" ), + DEFINE_KEYFIELD( m_difficulty, FIELD_INTEGER, "difficulty" ), + DEFINE_KEYFIELD( m_bRetainBuildings, FIELD_BOOLEAN, "retainBuildings" ), + DEFINE_KEYFIELD( m_bSpawnOnlyWhenTriggered, FIELD_BOOLEAN, "spawnOnlyWhenTriggered" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetSuppressFire", InputSetSuppressFire ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetDisableDodge", InputSetDisableDodge ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetDifficulty", InputSetDifficulty ), + DEFINE_INPUTFUNC( FIELD_STRING, "CommandGotoActionPoint", InputCommandGotoActionPoint ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetAttentionFocus", InputSetAttentionFocus ), + DEFINE_INPUTFUNC( FIELD_STRING, "ClearAttentionFocus", InputClearAttentionFocus ), + + DEFINE_INPUTFUNC( FIELD_VOID, "SpawnBot", InputSpawnBot ), + DEFINE_INPUTFUNC( FIELD_VOID, "RemoveBots", InputRemoveBots ), + + DEFINE_OUTPUT( m_onSpawned, "OnSpawned" ), + DEFINE_OUTPUT( m_onExpended, "OnExpended" ), + DEFINE_OUTPUT( m_onBotKilled, "OnBotKilled" ), + + DEFINE_THINKFUNC( GeneratorThink ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( bot_generator, CTFBotGenerator ); + +enum +{ + kOnDeath_Respawn, + kOnDeath_RemoveSelf, + kOnDeath_MoveToSpectatorTeam, +}; + +//------------------------------------------------------------------------------ +CTFBotGenerator::CTFBotGenerator( void ) + : m_bBotChoosesClass(false) + , m_bSuppressFire(false) + , m_bDisableDodge(false) + , m_bUseTeamSpawnpoint(false) + , m_bRetainBuildings(false) + , m_bExpended(false) + , m_iOnDeathAction(kOnDeath_RemoveSelf) + , m_difficulty(CTFBot::UNDEFINED) + , m_spawnCountRemaining(0) + , m_bSpawnOnlyWhenTriggered(false) + , m_bEnabled(true) +{ + SetThink( NULL ); +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::InputEnable( inputdata_t &inputdata ) +{ + m_bEnabled = true; + + if ( m_bExpended ) + { + return; + } + + SetThink( &CTFBotGenerator::GeneratorThink ); + + if ( m_spawnCountRemaining ) + { + // already generating - don't restart count + return; + } + SetNextThink( gpGlobals->curtime ); + m_spawnCountRemaining = m_spawnCount; +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::InputDisable( inputdata_t &inputdata ) +{ + m_bEnabled = false; + + // just stop thinking + SetThink( NULL ); +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::InputSetSuppressFire( inputdata_t &inputdata ) +{ + m_bSuppressFire = inputdata.value.Bool(); +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::InputSetDisableDodge( inputdata_t &inputdata ) +{ + m_bDisableDodge = inputdata.value.Bool(); +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::InputSetDifficulty( inputdata_t &inputdata ) +{ + m_difficulty = clamp( inputdata.value.Int(), (int) CTFBot::UNDEFINED, (int) CTFBot::EXPERT ); +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::InputCommandGotoActionPoint( inputdata_t &inputdata ) +{ + CTFBotActionPoint *pActionPoint = dynamic_cast<CTFBotActionPoint *>( gEntList.FindEntityByName( NULL, inputdata.value.String() ) ); + if ( pActionPoint == NULL ) + { + return; + } + for ( int i = 0; i < m_spawnedBotVector.Count(); ) + { + CHandle< CTFBot > hBot = m_spawnedBotVector[i]; + if ( hBot == NULL ) + { + m_spawnedBotVector.FastRemove(i); + continue; + } + if ( hBot->GetTeamNumber() == TEAM_SPECTATOR ) + { + m_spawnedBotVector.FastRemove(i); + continue; + } + hBot->SetActionPoint( pActionPoint ); + hBot->OnCommandString( "goto action point" ); + ++i; + } +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::InputSetAttentionFocus( inputdata_t &inputdata ) +{ + CBaseEntity *focus = gEntList.FindEntityByName( NULL, inputdata.value.String() ); + + if ( focus == NULL ) + { + return; + } + + for( int i = 0; i < m_spawnedBotVector.Count(); ) + { + CTFBot *bot = m_spawnedBotVector[i]; + + if ( !bot || bot->GetTeamNumber() == TEAM_SPECTATOR ) + { + m_spawnedBotVector.FastRemove(i); + continue; + } + + bot->SetAttentionFocus( focus ); + + ++i; + } +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::InputClearAttentionFocus( inputdata_t &inputdata ) +{ + for( int i = 0; i < m_spawnedBotVector.Count(); ) + { + CTFBot *bot = m_spawnedBotVector[i]; + + if ( !bot || bot->GetTeamNumber() == TEAM_SPECTATOR ) + { + m_spawnedBotVector.FastRemove(i); + continue; + } + + bot->ClearAttentionFocus(); + + ++i; + } +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::InputSpawnBot( inputdata_t &inputdata ) +{ + if ( m_bEnabled ) + { + SpawnBot(); + } +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::InputRemoveBots( inputdata_t &inputdata ) +{ + for( int i = 0; i < m_spawnedBotVector.Count(); i++ ) + { + CTFBot *pBot = m_spawnedBotVector[i]; + if ( pBot ) + { + pBot->Remove(); + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", pBot->GetUserID() ) ); + } + + m_spawnedBotVector.FastRemove(i); + } +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::OnBotKilled( CTFBot *pBot ) +{ + m_onBotKilled.FireOutput( pBot, this ); +} + +//------------------------------------------------------------------------------ + +void CTFBotGenerator::Activate() +{ + BaseClass::Activate(); + m_bBotChoosesClass = FStrEq( m_className.ToCStr(), "auto" ); + m_moveGoal = gEntList.FindEntityByName( NULL, m_actionPointName.ToCStr() ); +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::GeneratorThink( void ) +{ + // still waiting for the real game to start? + gamerules_roundstate_t roundState = TFGameRules()->State_Get(); + if ( roundState >= GR_STATE_TEAM_WIN || roundState < GR_STATE_PREROUND || TFGameRules()->IsInWaitingForPlayers() ) + { + SetNextThink( gpGlobals->curtime + 1.0f ); + return; + } + + // create the bot finally... + if ( !m_bSpawnOnlyWhenTriggered ) + { + SpawnBot(); + } +} + +//------------------------------------------------------------------------------ +void CTFBotGenerator::SpawnBot( void ) +{ + // did we exceed the max active count? + for ( int i = 0; i < m_spawnedBotVector.Count(); ) + { + CHandle< CTFBot > hBot = m_spawnedBotVector[i]; + if ( hBot == NULL ) + { + m_spawnedBotVector.FastRemove(i); + continue; + } + if ( hBot->GetTeamNumber() == TEAM_SPECTATOR ) + { + m_spawnedBotVector.FastRemove(i); + continue; + } + ++i; + } + + if ( m_spawnedBotVector.Count() >= m_maxActiveCount ) + { + SetNextThink( gpGlobals->curtime + 0.1f ); + return; + } + + char name[256]; + CTFBot *bot = TheTFBots().GetAvailableBotFromPool(); + if ( bot == NULL ) + { + CreateBotName( TEAM_UNASSIGNED, TF_CLASS_UNDEFINED, (CTFBot::DifficultyType)m_difficulty, name, sizeof(name) ); + bot = NextBotCreatePlayerBot< CTFBot >( name ); + } + + if ( bot ) + { + m_spawnedBotVector.AddToTail( bot ); + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() ) + { + bot->SetAttribute( CTFBot::IS_NPC ); + } +#endif // TF_RAID_MODE + + bot->SetSpawner( this ); + + if ( m_bUseTeamSpawnpoint == false ) + { + bot->SetSpawnPoint( this ); + } + + if ( m_bSuppressFire ) + { + bot->SetAttribute( CTFBot::SUPPRESS_FIRE ); + } + + if ( m_bRetainBuildings ) + { + bot->SetAttribute( CTFBot::RETAIN_BUILDINGS ); + } + + if ( m_bDisableDodge ) + { + bot->SetAttribute( CTFBot::DISABLE_DODGE ); + } + + if ( m_difficulty != CTFBot::UNDEFINED ) + { + bot->SetDifficulty( (CTFBot::DifficultyType )m_difficulty ); + } + + // propagate the generator's spawn flags into all bots generated + bot->ClearBehaviorFlag( TFBOT_ALL_BEHAVIOR_FLAGS ); + bot->SetBehaviorFlag( m_spawnflags ); + + switch ( m_iOnDeathAction ) + { + case kOnDeath_RemoveSelf: + bot->SetAttribute( CTFBot::REMOVE_ON_DEATH ); + break; + case kOnDeath_MoveToSpectatorTeam: + bot->SetAttribute( CTFBot::BECOME_SPECTATOR_ON_DEATH ); + break; + } // switch + + bot->SetActionPoint( dynamic_cast<CTFBotActionPoint *>( m_moveGoal.Get() ) ); + + // pick a team and force the team change + // HandleCommand_JoinTeam() may fail, but this should always succeed + int iTeam = TEAM_UNASSIGNED; + if ( FStrEq( m_teamName.ToCStr(), "auto" ) ) + { + iTeam = bot->GetAutoTeam(); + } + else if ( FStrEq( m_teamName.ToCStr(), "spectate" ) ) + { + iTeam = TEAM_SPECTATOR; + } + else + { + for ( int i = 0; i < TF_TEAM_COUNT; ++i ) + { + COMPILE_TIME_ASSERT( TF_TEAM_COUNT == ARRAYSIZE( g_aTeamNames ) ); + if ( FStrEq( m_teamName.ToCStr(), g_aTeamNames[i] ) ) + { + iTeam = i; + break; + } + } + } + if ( iTeam == TEAM_UNASSIGNED ) + { + iTeam = bot->GetAutoTeam(); + } + bot->ChangeTeam( iTeam, false, false ); + + const char* pClassName = m_bBotChoosesClass ? bot->GetNextSpawnClassname() : m_className.ToCStr(); + bot->HandleCommand_JoinClass( pClassName ); + + // in training, reset the after the bot joins the class + if ( TFGameRules()->IsInTraining() ) + { + CTFBot::DifficultyType skill = bot->GetDifficulty(); + CreateBotName( iTeam, bot->GetPlayerClass()->GetClassIndex(), skill, name, sizeof(name) ); + engine->SetFakeClientConVarValue( bot->edict(), "name", name ); + } + + if ( bot->IsAlive() == false ) + { + bot->ForceRespawn(); + } + + // make sure the bot is facing the right way. + // @todo Tom Bui: for some reason it is still turning towards another direction...need to investigate + bot->SnapEyeAngles( GetAbsAngles() ); + + if ( FStrEq( m_initialCommand.ToCStr(), "" ) == false ) + { + // @note Tom Bui: we call Update() once here to make sure the bot is ready to receive commands + bot->Update(); + bot->OnCommandString( m_initialCommand.ToCStr() ); + } + m_onSpawned.FireOutput( bot, this ); + + --m_spawnCountRemaining; + if ( m_spawnCountRemaining ) + { + SetNextThink( gpGlobals->curtime + m_spawnInterval ); + } + else + { + SetThink( NULL ); + m_onExpended.FireOutput( this, this ); + m_bExpended = true; + } + } +} + +//------------------------------------------------------------------------------ + +BEGIN_DATADESC( CTFBotActionPoint ) + DEFINE_KEYFIELD( m_stayTime, FIELD_FLOAT, "stay_time" ), + DEFINE_KEYFIELD( m_desiredDistance, FIELD_FLOAT, "desired_distance" ), + DEFINE_KEYFIELD( m_nextActionPointName, FIELD_STRING, "next_action_point" ), + DEFINE_KEYFIELD( m_command, FIELD_STRING, "command" ), + DEFINE_OUTPUT( m_onReachedActionPoint, "OnBotReached" ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( bot_action_point, CTFBotActionPoint ); + +//------------------------------------------------------------------------------ + +CTFBotActionPoint::CTFBotActionPoint() +: m_stayTime( 0.0f ) +, m_desiredDistance( 1.0f ) + +{ + +} + +//------------------------------------------------------------------------------ + +void CTFBotActionPoint::Activate() +{ + BaseClass::Activate(); + m_moveGoal = gEntList.FindEntityByName( NULL, m_nextActionPointName.ToCStr() ); +} + +//------------------------------------------------------------------------------ + +bool CTFBotActionPoint::IsWithinRange( CBaseEntity *entity ) +{ + return ( entity->GetAbsOrigin() - GetAbsOrigin() ).IsLengthLessThan( m_desiredDistance ); +} + +//------------------------------------------------------------------------------ + +void CTFBotActionPoint::ReachedActionPoint( CTFBot* pBot ) +{ + if ( FStrEq( m_command.ToCStr(), "" ) == false ) + { + pBot->OnCommandString( m_command.ToCStr() ); + } + m_onReachedActionPoint.FireOutput( pBot, this ); +} + +//------------------------------------------------------------------------------ diff --git a/game/server/tf/bot/map_entities/tf_bot_generator.h b/game/server/tf/bot/map_entities/tf_bot_generator.h new file mode 100644 index 0000000..dfbc52a --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_generator.h @@ -0,0 +1,100 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_generator.h +// Entity to spawn a collection of TFBots +// Michael Booth, September 2009 + +#ifndef TF_BOT_GENERATOR_H +#define TF_BOT_GENERATOR_H + +#include "bot/tf_bot.h" + + +class CTFBotGenerator : public CPointEntity +{ +public: + DECLARE_CLASS( CTFBotGenerator, CPointEntity ); + DECLARE_DATADESC(); + + CTFBotGenerator( void ); + virtual ~CTFBotGenerator() { } + + virtual void Activate(); + + void GeneratorThink( void ); + void SpawnBot( void ); + + // Input. + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputSetSuppressFire( inputdata_t &inputdata ); + void InputSetDisableDodge( inputdata_t &inputdata ); + void InputSetDifficulty( inputdata_t &inputdata ); + void InputCommandGotoActionPoint( inputdata_t &inputdata ); + void InputSetAttentionFocus( inputdata_t &inputdata ); + void InputClearAttentionFocus( inputdata_t &inputdata ); + void InputSpawnBot( inputdata_t &inputdata ); + void InputRemoveBots( inputdata_t &inputdata ); + + // Output + void OnBotKilled( CTFBot *pBot ); + +private: + bool m_bBotChoosesClass; + bool m_bSuppressFire; + bool m_bDisableDodge; + bool m_bUseTeamSpawnpoint; + bool m_bRetainBuildings; + bool m_bExpended; + int m_iOnDeathAction; + int m_spawnCount; + int m_spawnCountRemaining; + int m_maxActiveCount; + float m_spawnInterval; + string_t m_className; + string_t m_teamName; + string_t m_actionPointName; + string_t m_initialCommand; + CHandle< CBaseEntity > m_moveGoal; + int m_difficulty; + bool m_bSpawnOnlyWhenTriggered; + bool m_bEnabled; + + COutputEvent m_onSpawned; + COutputEvent m_onExpended; + COutputEvent m_onBotKilled; + + CUtlVector< CHandle< CTFBot > > m_spawnedBotVector; +}; + +//--------------------------------------------------------------- +// +// Bot generator may have one of these as an argument, which +// means "tell the bot I created to move here and do what this node says". +// Things like "stay here", "move to <next task point>", "face towards <X>", "shoot at <Y>", etc +// +class CTFBotActionPoint : public CPointEntity +{ + DECLARE_CLASS( CTFBotActionPoint, CPointEntity ); +public: + DECLARE_DATADESC(); + + CTFBotActionPoint( void ); + virtual ~CTFBotActionPoint() { } + + virtual void Activate(); + + bool IsWithinRange( CBaseEntity *entity ); + void ReachedActionPoint( CTFBot* pBot ); + + CHandle< CBaseEntity > m_moveGoal; + + // reflected + float m_stayTime; + float m_desiredDistance; + string_t m_nextActionPointName; + string_t m_command; + + COutputEvent m_onReachedActionPoint; +}; + +#endif // TF_BOT_GENERATOR_H diff --git a/game/server/tf/bot/map_entities/tf_bot_hint.cpp b/game/server/tf/bot/map_entities/tf_bot_hint.cpp new file mode 100644 index 0000000..b4dfa11 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_hint.cpp @@ -0,0 +1,128 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_hint.cpp +// Designer-placed hint for TFBots + +#include "cbase.h" +#include "bot/tf_bot.h" +#include "tf_bot_hint.h" + +BEGIN_DATADESC( CTFBotHint ) + DEFINE_KEYFIELD( m_team, FIELD_INTEGER, "team" ), + DEFINE_KEYFIELD( m_hint, FIELD_INTEGER, "hint" ), + DEFINE_KEYFIELD( m_isDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( func_tfbot_hint, CTFBotHint ); + +// +// NOTE: For simplicity and runtime efficiency, this will not +// play nice with nav area hints stored in the mesh, +// nor will overlapping hints of the same type work well. +// + +//------------------------------------------------------------------------------ +CTFBotHint::CTFBotHint( void ) +{ + m_isDisabled = false; +} + + +//-------------------------------------------------------------------------------------------------------- +// Return true if this hint applies to the given entity +bool CTFBotHint::IsFor( CTFBot *who ) const +{ + if ( m_isDisabled ) + { + return false; + } + + if ( m_team > 0 && who->GetTeamNumber() != m_team ) + { + return false; + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFBotHint::Spawn( void ) +{ + BaseClass::Spawn(); + + SetSolid( SOLID_BSP ); + AddSolidFlags( FSOLID_NOT_SOLID ); + + SetMoveType( MOVETYPE_NONE ); + SetModel( STRING( GetModelName() ) ); + AddEffects( EF_NODRAW ); + SetCollisionGroup( COLLISION_GROUP_NONE ); + + VPhysicsInitShadow( false, false ); + + UpdateNavDecoration(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFBotHint::UpdateOnRemove( void ) +{ + BaseClass::UpdateOnRemove(); + + UpdateNavDecoration(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFBotHint::InputEnable( inputdata_t &inputdata ) +{ + m_isDisabled = false; + UpdateNavDecoration(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFBotHint::InputDisable( inputdata_t &inputdata ) +{ + m_isDisabled = true; + UpdateNavDecoration(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFBotHint::UpdateNavDecoration( void ) +{ + Extent extent; + extent.Init( this ); + + CUtlVector< CTFNavArea * > overlapVector; + TheNavMesh->CollectAreasOverlappingExtent( extent, &overlapVector ); + + int attributeBits = 0; + switch( m_hint ) + { + case HINT_SNIPER_SPOT: + attributeBits = TF_NAV_SNIPER_SPOT; + break; + + case HINT_SENTRY_SPOT: + attributeBits = TF_NAV_SENTRY_SPOT; + break; + } + + for( int j=0; j<overlapVector.Count(); ++j ) + { + if ( m_isDisabled ) + { + overlapVector[j]->ClearAttributeTF( attributeBits ); + } + else + { + overlapVector[j]->SetAttributeTF( attributeBits ); + } + } +} + + diff --git a/game/server/tf/bot/map_entities/tf_bot_hint.h b/game/server/tf/bot/map_entities/tf_bot_hint.h new file mode 100644 index 0000000..4945145 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_hint.h @@ -0,0 +1,54 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_hint.h +// Designer-placed hint for TFBots + +#ifndef TF_BOT_HINT_H +#define TF_BOT_HINT_H + +class CTFBot; + +//----------------------------------------------------------------------------------------------------- +/** + * An entity that specifies TFBot behavior hints. + */ +class CTFBotHint : public CBaseEntity +{ +public: + DECLARE_DATADESC(); + DECLARE_CLASS( CTFBotHint, CBaseEntity ); + + CTFBotHint( void ); + virtual ~CTFBotHint() { } + + enum HintType + { + HINT_SNIPER_SPOT = 0, + HINT_SENTRY_SPOT = 1, + }; + + bool IsA( HintType type ) const; + + bool IsFor( CTFBot *who ) const; // return true if this hint applies to the given entity + + virtual void Spawn( void ); + virtual void UpdateOnRemove( void ); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + bool IsEnabled( void ) const { return !m_isDisabled; } + +protected: + int m_team; + int m_hint; + bool m_isDisabled; + + void UpdateNavDecoration( void ); +}; + +inline bool CTFBotHint::IsA( HintType type ) const +{ + return ( m_hint == type ); +} + + +#endif // TF_BOT_HINT_H diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.cpp b/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.cpp new file mode 100644 index 0000000..7f21a71 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.cpp @@ -0,0 +1,160 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" +#include "tf_bot_hint_engineer_nest.h" +#include "tf_obj.h" +#include "tf_obj_teleporter.h" + +IMPLEMENT_SERVERCLASS_ST( CTFBotHintEngineerNest, DT_TFBotHintEngineerNest ) + SendPropBool( SENDINFO(m_bHasActiveTeleporter) ), +END_SEND_TABLE() + +BEGIN_DATADESC( CTFBotHintEngineerNest ) +END_DATADESC() + +LINK_ENTITY_TO_CLASS( bot_hint_engineer_nest, CTFBotHintEngineerNest ); + +//------------------------------------------------------------------------------ +CTFBotHintEngineerNest::CTFBotHintEngineerNest( void ) +{ + m_bHasActiveTeleporter = false; +} + + +void CTFBotHintEngineerNest::Spawn() +{ + BaseClass::Spawn(); + + SetThink( &CTFBotHintEngineerNest::HintThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +void CTFBotHintEngineerNest::HintThink() +{ + // find sentry and teleporter hint + for ( int i=0; i<ITFBotHintEntityAutoList::AutoList().Count(); ++i ) + { + CBaseTFBotHintEntity *pHint = static_cast< CBaseTFBotHintEntity* >( ITFBotHintEntityAutoList::AutoList()[i] ); + if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_SENTRYGUN ) && pHint->GetEntityName() == GetEntityName() ) + { + m_sentries.AddToTail( pHint ); + } + else if ( pHint->IsHintType( CBaseTFBotHintEntity::HINT_TELEPORTER_EXIT ) && pHint->GetEntityName() == GetEntityName() ) + { + m_teleporters.AddToTail( pHint ); + } + } + + if ( m_sentries.Count() == 0 && m_teleporters.Count() == 0 ) + { + AssertMsg( 0, "Must have a teleporter and/or a sentry hint with the same name." ); + Warning( "Must have a teleporter and/or a sentry hint with the same name.\n" ); + } + + SetThink( &CTFBotHintEngineerNest::HintTeleporterThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +void CTFBotHintEngineerNest::HintTeleporterThink() +{ + bool bFoundActiveTeleporter = false; + for ( int i=0; i<m_teleporters.Count(); ++i ) + { + CBaseEntity* pOwner = m_teleporters[i]->GetOwnerEntity(); + if ( pOwner && pOwner->IsBaseObject() ) + { + CObjectTeleporter *pTeleporter = assert_cast< CObjectTeleporter* >( pOwner ); + if ( pTeleporter ) + { + bFoundActiveTeleporter |= !pTeleporter->IsBuilding(); + } + } + } + + // update particle bool + m_bHasActiveTeleporter = bFoundActiveTeleporter; + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +bool CTFBotHintEngineerNest::IsStaleNest() const +{ + for ( int i=0; i<m_sentries.Count(); ++i ) + { + if ( m_sentries[i]->OwnerObjectHasNoOwner() ) + { + return true; + } + } + + for ( int i=0; i<m_teleporters.Count(); ++i ) + { + if ( m_teleporters[i]->OwnerObjectHasNoOwner() ) + { + return true; + } + } + + return false; +} + + +void CTFBotHintEngineerNest::DetonateStaleNest() +{ + DetonateObjectsFromHints( m_sentries ); + DetonateObjectsFromHints( m_teleporters ); +} + + +void CTFBotHintEngineerNest::DetonateObjectsFromHints( const HintVector_t& hints ) +{ + for ( int i=0; i<hints.Count(); ++i ) + { + if ( hints[i]->OwnerObjectHasNoOwner() ) + { + CBaseObject* pObj = assert_cast< CBaseObject* >( hints[i]->GetOwnerEntity() ); + if ( pObj ) + { + pObj->DetonateObject(); + } + } + } +} + + +CBaseTFBotHintEntity* CTFBotHintEngineerNest::GetHint( const HintVector_t& hints ) const +{ + if ( hints.Count() == 0 ) + { + return NULL; + } + + for ( int i=0; i<hints.Count(); ++i ) + { + if ( hints[i]->OwnerObjectHasNoOwner() ) + { + return hints[i]; + } + } + + int which = RandomInt( 0, hints.Count() - 1 ); + return hints[ which ]; +} + + +CTFBotHintSentrygun* CTFBotHintEngineerNest::GetSentryHint() const +{ + return (CTFBotHintSentrygun*)GetHint( m_sentries ); +} + + +CTFBotHintTeleporterExit* CTFBotHintEngineerNest::GetTeleporterHint() const +{ + return (CTFBotHintTeleporterExit*)GetHint( m_teleporters ); +} diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.h b/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.h new file mode 100644 index 0000000..9e5fdf5 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_hint_engineer_nest.h @@ -0,0 +1,53 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef TF_BOT_HINT_ENGINEER_NEST_H +#define TF_BOT_HINT_ENGINEER_NEST_H + +#include "tf_bot_hint_entity.h" + +typedef CUtlVector< CHandle< CBaseTFBotHintEntity > > HintVector_t; + +class CTFBotHintSentrygun; +class CTFBotHintTeleporterExit; + +class CTFBotHintEngineerNest : public CBaseTFBotHintEntity +{ + DECLARE_CLASS( CTFBotHintEngineerNest, CBaseTFBotHintEntity ); +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CTFBotHintEngineerNest( void ); + virtual ~CTFBotHintEngineerNest() { } + + virtual void Spawn() OVERRIDE; + + virtual HintType GetHintType() const OVERRIDE { return HINT_ENGINEER_NEST; } + + virtual int UpdateTransmitState() + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } + + void HintThink(); + void HintTeleporterThink(); + + bool IsStaleNest() const; + void DetonateStaleNest(); + + CTFBotHintSentrygun* GetSentryHint() const; + CTFBotHintTeleporterExit* GetTeleporterHint() const; +private: + void DetonateObjectsFromHints( const HintVector_t& hints ); + CBaseTFBotHintEntity* GetHint( const HintVector_t& hints ) const; + + HintVector_t m_sentries; + HintVector_t m_teleporters; + + CNetworkVar( bool, m_bHasActiveTeleporter ); +}; + +#endif // TF_BOT_HINT_ENGINEER_NEST_H diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_entity.cpp b/game/server/tf/bot/map_entities/tf_bot_hint_entity.cpp new file mode 100644 index 0000000..ffd2c74 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_hint_entity.cpp @@ -0,0 +1,60 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#include "cbase.h" +#include "tf_bot_hint_entity.h" +#include "tf_obj.h" +#include "tf_player.h" + + +BEGIN_DATADESC( CBaseTFBotHintEntity ) + DEFINE_KEYFIELD( m_isDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), +END_DATADESC() + +IMPLEMENT_AUTO_LIST( ITFBotHintEntityAutoList ); + +//------------------------------------------------------------------------------ +CBaseTFBotHintEntity::CBaseTFBotHintEntity( void ) + : m_isDisabled( false ), + m_hintType( HINT_INVALID ) +{ +} + + +bool CBaseTFBotHintEntity::OwnerObjectHasNoOwner() const +{ + CBaseEntity* pOwner = GetOwnerEntity(); + if ( pOwner && pOwner->IsBaseObject() ) + { + CBaseObject *pObj = static_cast< CBaseObject* >( pOwner ); + if ( pObj->GetBuilder() == NULL ) + { + return true; + } + else + { + if ( !pObj->GetBuilder()->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + AssertMsg( 0, "Object has an owner that's not engineer." ); + Warning( "Object has an owner that's not engineer." ); + } + } + } + return false; +} + + +bool CBaseTFBotHintEntity::OwnerObjectFinishBuilding() const +{ + CBaseEntity* pOwner = GetOwnerEntity(); + if ( pOwner && pOwner->IsBaseObject() ) + { + CBaseObject *pObj = static_cast< CBaseObject* >( pOwner ); + return !pObj->IsBuilding(); + } + return false; +} diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_entity.h b/game/server/tf/bot/map_entities/tf_bot_hint_entity.h new file mode 100644 index 0000000..af0df70 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_hint_entity.h @@ -0,0 +1,59 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= +#ifndef TF_BOT_HINT_ENTITY_H +#define TF_BOT_HINT_ENTITY_H + +DECLARE_AUTO_LIST( ITFBotHintEntityAutoList ); + +class CBaseTFBotHintEntity : public CPointEntity, public ITFBotHintEntityAutoList +{ + DECLARE_CLASS( CBaseTFBotHintEntity, CPointEntity ); +public: + DECLARE_DATADESC(); + + CBaseTFBotHintEntity( void ); + virtual ~CBaseTFBotHintEntity() { } + + enum HintType + { + HINT_INVALID = -1, + HINT_TELEPORTER_EXIT, + HINT_SENTRYGUN, + HINT_ENGINEER_NEST, + }; + virtual HintType GetHintType() const = 0; + bool IsHintType( HintType hintType ) { return GetHintType() == hintType; } + + bool OwnerObjectHasNoOwner() const; + bool OwnerObjectFinishBuilding() const; + + bool IsEnabled() const; + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + +private: + + bool m_isDisabled; + HintType m_hintType; +}; + + +inline void CBaseTFBotHintEntity::InputEnable( inputdata_t &inputdata ) +{ + m_isDisabled = false; +} + +inline void CBaseTFBotHintEntity::InputDisable( inputdata_t &inputdata ) +{ + m_isDisabled = true; +} + +inline bool CBaseTFBotHintEntity::IsEnabled() const +{ + return !m_isDisabled; +} + +#endif // TF_BOT_HINT_ENTITY_H diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.cpp b/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.cpp new file mode 100644 index 0000000..4cc804d --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.cpp @@ -0,0 +1,42 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_hint_sentrygun.cpp +// Designer-placed hint for bot sentry placement +// Michael Booth, October 2009 + +#include "cbase.h" +#include "bot/tf_bot.h" +#include "tf_bot_hint_sentrygun.h" + + +BEGIN_DATADESC( CTFBotHintSentrygun ) + DEFINE_KEYFIELD( m_isSticky, FIELD_BOOLEAN, "sticky" ), + DEFINE_OUTPUT( m_outputOnSentryGunDestroyed, "OnSentryGunDestroyed" ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( bot_hint_sentrygun, CTFBotHintSentrygun ); + +//------------------------------------------------------------------------------ +CTFBotHintSentrygun::CTFBotHintSentrygun( void ) + : m_isSticky( false ) + , m_iUseCount( 0 ) +{ +} + +//------------------------------------------------------------------------------ +void CTFBotHintSentrygun::OnSentryGunDestroyed( CBaseEntity *pEntity ) +{ + m_outputOnSentryGunDestroyed.FireOutput( pEntity, pEntity ); +} + +//------------------------------------------------------------------------------ +bool CTFBotHintSentrygun::IsAvailableForSelection( CTFPlayer *pRequestingPlayer ) const +{ + // sentry hint is eligible as long as there is no owner (or the owner is no longer an engineer) + // if the hint is enabled and the hint is not in use and it is on the same team as me + if ( ( GetPlayerOwner() == NULL || !GetPlayerOwner()->IsPlayerClass( TF_CLASS_ENGINEER ) ) && + ( IsEnabled() && IsInUse() == false && InSameTeam( pRequestingPlayer ) ) ) + { + return true; + } + return false; +} diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.h b/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.h new file mode 100644 index 0000000..e2b0440 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_hint_sentrygun.h @@ -0,0 +1,75 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_hint_sentrygun.h +// Designer-placed hint for bot sentry placement +// Michael Booth, October 2009 + +#ifndef TF_BOT_HINT_SENTRYGUN_H +#define TF_BOT_HINT_SENTRYGUN_H + +#include "tf_bot_hint_entity.h" + +class CTFPlayer; + +class CTFBotHintSentrygun : public CBaseTFBotHintEntity +{ +public: + DECLARE_CLASS( CTFBotHintSentrygun, CBaseTFBotHintEntity ); + DECLARE_DATADESC(); + + CTFBotHintSentrygun( void ); + virtual ~CTFBotHintSentrygun() { } + + bool IsSticky() const; + bool IsInUse() const; + + CTFPlayer *GetPlayerOwner() const; + void SetPlayerOwner( CTFPlayer *pPlayerOwner ); + + void IncrementUseCount(); + void DecrementUseCount(); + + void OnSentryGunDestroyed( CBaseEntity *pBaseEntity ); + + bool IsAvailableForSelection( CTFPlayer *pRequestingPlayer ) const; + + virtual HintType GetHintType() const OVERRIDE { return HINT_SENTRYGUN; } + +private: + bool m_isSticky; + int m_iUseCount; + COutputEvent m_outputOnSentryGunDestroyed; + + CHandle< CTFPlayer > m_playerOwner; +}; + +inline bool CTFBotHintSentrygun::IsSticky() const +{ + return m_isSticky; +} + +inline bool CTFBotHintSentrygun::IsInUse() const +{ + return m_iUseCount != 0; +} + +inline CTFPlayer *CTFBotHintSentrygun::GetPlayerOwner() const +{ + return m_playerOwner; +} + +inline void CTFBotHintSentrygun::SetPlayerOwner( CTFPlayer *pPlayerOwner ) +{ + m_playerOwner = pPlayerOwner; +} + +inline void CTFBotHintSentrygun::IncrementUseCount() +{ + ++m_iUseCount; +} + +inline void CTFBotHintSentrygun::DecrementUseCount() +{ + --m_iUseCount; +} + +#endif // TF_BOT_HINT_SENTRYGUN_H diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.cpp b/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.cpp new file mode 100644 index 0000000..32beca2 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.cpp @@ -0,0 +1,19 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_hint_teleporter_exit.cpp +// Designer-placed hint for bot teleporter exit placement +// Michael Booth, May 2010 + +#include "cbase.h" +#include "tf_bot_hint_teleporter_exit.h" + + +BEGIN_DATADESC( CTFBotHintTeleporterExit ) +END_DATADESC() + +LINK_ENTITY_TO_CLASS( bot_hint_teleporter_exit, CTFBotHintTeleporterExit ); + +//------------------------------------------------------------------------------ +CTFBotHintTeleporterExit::CTFBotHintTeleporterExit( void ) +{ +} + diff --git a/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.h b/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.h new file mode 100644 index 0000000..a1af04a --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_hint_teleporter_exit.h @@ -0,0 +1,23 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_hint_teleporter_exit.h +// Designer-placed hint for bot teleporter exit placement +// Michael Booth, May 2010 + +#ifndef TF_BOT_HINT_TELEPORTER_EXIT_H +#define TF_BOT_HINT_TELEPORTER_EXIT_H + +#include "tf_bot_hint_entity.h" + +class CTFBotHintTeleporterExit : public CBaseTFBotHintEntity +{ + DECLARE_CLASS( CTFBotHintTeleporterExit, CBaseTFBotHintEntity ); +public: + DECLARE_DATADESC(); + + CTFBotHintTeleporterExit( void ); + virtual ~CTFBotHintTeleporterExit() { } + + virtual HintType GetHintType() const OVERRIDE { return HINT_TELEPORTER_EXIT; } +}; + +#endif // TF_BOT_HINT_TELEPORTER_EXIT_H diff --git a/game/server/tf/bot/map_entities/tf_bot_proxy.cpp b/game/server/tf/bot/map_entities/tf_bot_proxy.cpp new file mode 100644 index 0000000..560b341 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_proxy.cpp @@ -0,0 +1,167 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_proxy.cpp +// A Hammer entity that spawns a TFBot and relays events to/from it +// Michael Booth, November 2009 + +#include "cbase.h" + +#include "bot/tf_bot.h" +#include "tf_bot_proxy.h" +#include "tf_bot_generator.h" + + +BEGIN_DATADESC( CTFBotProxy ) + DEFINE_KEYFIELD( m_botName, FIELD_STRING, "bot_name" ), + DEFINE_KEYFIELD( m_className, FIELD_STRING, "class" ), + DEFINE_KEYFIELD( m_teamName, FIELD_STRING, "team" ), + DEFINE_KEYFIELD( m_respawnInterval, FIELD_FLOAT, "respawn_interval" ), + DEFINE_KEYFIELD( m_actionPointName, FIELD_STRING, "action_point" ), + DEFINE_KEYFIELD( m_spawnOnStart, FIELD_STRING, "spawn_on_start" ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetTeam", InputSetTeam ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetClass", InputSetClass ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetMovementGoal", InputSetMovementGoal ), + DEFINE_INPUTFUNC( FIELD_VOID, "Spawn", InputSpawn ), + DEFINE_INPUTFUNC( FIELD_VOID, "Delete", InputDelete ), + + DEFINE_OUTPUT( m_onSpawned, "OnSpawned" ), + DEFINE_OUTPUT( m_onInjured, "OnInjured" ), + DEFINE_OUTPUT( m_onKilled, "OnKilled" ), + DEFINE_OUTPUT( m_onAttackingEnemy, "OnAttackingEnemy" ), + DEFINE_OUTPUT( m_onKilledEnemy, "OnKilledEnemy" ), + + DEFINE_THINKFUNC( Think ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( bot_proxy, CTFBotProxy ); + + + +//------------------------------------------------------------------------------ +CTFBotProxy::CTFBotProxy( void ) +{ + V_strcpy_safe( m_botName, "TFBot" ); + V_strcpy_safe( m_teamName, "auto" ); + V_strcpy_safe( m_className, "auto" ); + m_bot = NULL; + m_moveGoal = NULL; + SetThink( NULL ); +} + + +//------------------------------------------------------------------------------ +void CTFBotProxy::Think( void ) +{ + +} + + +//------------------------------------------------------------------------------ +void CTFBotProxy::InputSetTeam( inputdata_t &inputdata ) +{ + const char *teamName = inputdata.value.String(); + if ( teamName && teamName[0] ) + { + V_strcpy_safe( m_teamName, teamName ); + + // if m_bot exists, tell it to change team + if ( m_bot != NULL ) + { + m_bot->HandleCommand_JoinTeam( m_teamName ); + } + } +} + + +//------------------------------------------------------------------------------ +void CTFBotProxy::InputSetClass( inputdata_t &inputdata ) +{ + const char *className = inputdata.value.String(); + if ( className && className[0] ) + { + V_strcpy_safe( m_className, className ); + + // if m_bot exists, tell it to change class + if ( m_bot != NULL ) + { + m_bot->HandleCommand_JoinClass( m_className ); + } + } +} + + +//------------------------------------------------------------------------------ +void CTFBotProxy::InputSetMovementGoal( inputdata_t &inputdata ) +{ + const char *entityName = inputdata.value.String(); + if ( entityName && entityName[0] ) + { + m_moveGoal = dynamic_cast< CTFBotActionPoint * >( gEntList.FindEntityByName( NULL, entityName ) ); + + // if m_bot exists, tell it to move to the new action point + if ( m_bot != NULL ) + { + m_bot->SetActionPoint( (CTFBotActionPoint *)m_moveGoal.Get() ); + } + } +} + + +//------------------------------------------------------------------------------ +void CTFBotProxy::InputSpawn( inputdata_t &inputdata ) +{ + m_bot = NextBotCreatePlayerBot< CTFBot >( m_botName ); + if ( m_bot != NULL ) + { + m_bot->SetSpawnPoint( this ); + m_bot->SetAttribute( CTFBot::REMOVE_ON_DEATH ); + m_bot->SetAttribute( CTFBot::IS_NPC ); + + m_bot->SetActionPoint( (CTFBotActionPoint *)m_moveGoal.Get() ); + + m_bot->HandleCommand_JoinTeam( m_teamName ); + m_bot->HandleCommand_JoinClass( m_className ); + + m_onSpawned.FireOutput( m_bot, m_bot ); + } +} + + +//------------------------------------------------------------------------------ +void CTFBotProxy::InputDelete( inputdata_t &inputdata ) +{ + if ( m_bot != NULL ) + { + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", m_bot->GetUserID() ) ); + m_bot = NULL; + } +} + + +//------------------------------------------------------------------------------ +void CTFBotProxy::OnInjured( void ) +{ + m_onInjured.FireOutput( this, this ); +} + + +//------------------------------------------------------------------------------ +void CTFBotProxy::OnKilled( void ) +{ + m_onKilled.FireOutput( this, this ); +} + + +//------------------------------------------------------------------------------ +void CTFBotProxy::OnAttackingEnemy( void ) +{ + m_onAttackingEnemy.FireOutput( this, this ); +} + + +//------------------------------------------------------------------------------ +void CTFBotProxy::OnKilledEnemy( void ) +{ + m_onKilledEnemy.FireOutput( this, this ); +} + diff --git a/game/server/tf/bot/map_entities/tf_bot_proxy.h b/game/server/tf/bot/map_entities/tf_bot_proxy.h new file mode 100644 index 0000000..6cdfbf4 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_proxy.h @@ -0,0 +1,58 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_proxy.h +// A Hammer entity that spawns a TFBot and relays events to/from it +// Michael Booth, November 2009 + +#ifndef TF_BOT_PROXY_H +#define TF_BOT_PROXY_H + + +class CTFBot; +class CTFBotActionPoint; + + +class CTFBotProxy : public CPointEntity +{ + DECLARE_CLASS( CTFBotProxy, CPointEntity ); +public: + DECLARE_DATADESC(); + + CTFBotProxy( void ); + virtual ~CTFBotProxy() { } + + void Think( void ); + + // Input + void InputSetTeam( inputdata_t &inputdata ); + void InputSetClass( inputdata_t &inputdata ); + void InputSetMovementGoal( inputdata_t &inputdata ); + void InputSpawn( inputdata_t &inputdata ); + void InputDelete( inputdata_t &inputdata ); + + void OnInjured( void ); + void OnKilled( void ); + void OnAttackingEnemy( void ); + void OnKilledEnemy( void ); + +protected: + // Output + COutputEvent m_onSpawned; + COutputEvent m_onInjured; + COutputEvent m_onKilled; + COutputEvent m_onAttackingEnemy; + COutputEvent m_onKilledEnemy; + + char m_botName[64]; + char m_className[64]; + char m_teamName[64]; + + string_t m_spawnOnStart; + string_t m_actionPointName; + float m_respawnInterval; + + CHandle< CTFBot > m_bot; + CHandle< CTFBotActionPoint > m_moveGoal; +}; + + +#endif // TF_BOT_PROXY_H diff --git a/game/server/tf/bot/map_entities/tf_bot_roster.cpp b/game/server/tf/bot/map_entities/tf_bot_roster.cpp new file mode 100644 index 0000000..d07fc23 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_roster.cpp @@ -0,0 +1,107 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_roster.cpp +// entity that dictates what classes a bot can choose when spawning +// Tom Bui, April 2010 + +#include "cbase.h" + +#include "tf_shareddefs.h" +#include "bot/map_entities/tf_bot_roster.h" + +//------------------------------------------------------------------------------ + +BEGIN_DATADESC( CTFBotRoster ) + DEFINE_KEYFIELD( m_teamName, FIELD_STRING, "team" ), + DEFINE_KEYFIELD( m_bAllowClassChanges, FIELD_BOOLEAN, "allowClassChanges" ), + DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_SCOUT], FIELD_BOOLEAN, "allowScout" ), + DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_SNIPER], FIELD_BOOLEAN, "allowSniper" ), + DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_SOLDIER], FIELD_BOOLEAN, "allowSoldier" ), + DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_DEMOMAN], FIELD_BOOLEAN, "allowDemoman" ), + DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_MEDIC], FIELD_BOOLEAN, "allowMedic" ), + DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_HEAVYWEAPONS], FIELD_BOOLEAN, "allowHeavy" ), + DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_PYRO], FIELD_BOOLEAN, "allowPyro" ), + DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_SPY], FIELD_BOOLEAN, "allowSpy" ), + DEFINE_KEYFIELD( m_bAllowedClasses[TF_CLASS_ENGINEER], FIELD_BOOLEAN, "allowEngineer" ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetTeam", InputSetTeam ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowScout", InputSetAllowScout ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowSniper", InputSetAllowSniper ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowSoldier", InputSetAllowSoldier ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowDemoman", InputSetAllowDemoman ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowMedic", InputSetAllowMedic ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowHeavy", InputSetAllowHeavy ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowPyro", InputSetAllowPyro ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowSpy", InputSetAllowSpy ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowEngineer", InputSetAllowEngineer ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( bot_roster, CTFBotRoster ); + +//------------------------------------------------------------------------------ + +CTFBotRoster::CTFBotRoster() +{ + memset( m_bAllowedClasses, 0, sizeof( m_bAllowedClasses ) ); +} + +//------------------------------------------------------------------------------ + +void CTFBotRoster::InputSetAllowScout( inputdata_t &inputdata ) +{ + m_bAllowedClasses[TF_CLASS_SCOUT] = inputdata.value.Bool(); +} + +void CTFBotRoster::InputSetAllowSniper( inputdata_t &inputdata ) +{ + m_bAllowedClasses[TF_CLASS_SNIPER] = inputdata.value.Bool(); +} + +void CTFBotRoster::InputSetAllowSoldier( inputdata_t &inputdata ) +{ + m_bAllowedClasses[TF_CLASS_SOLDIER] = inputdata.value.Bool(); +} + +void CTFBotRoster::InputSetAllowDemoman( inputdata_t &inputdata ) +{ + m_bAllowedClasses[TF_CLASS_DEMOMAN] = inputdata.value.Bool(); +} + +void CTFBotRoster::InputSetAllowMedic( inputdata_t &inputdata ) +{ + m_bAllowedClasses[TF_CLASS_MEDIC] = inputdata.value.Bool(); +} + +void CTFBotRoster::InputSetAllowHeavy( inputdata_t &inputdata ) +{ + m_bAllowedClasses[TF_CLASS_HEAVYWEAPONS] = inputdata.value.Bool(); +} + +void CTFBotRoster::InputSetAllowPyro( inputdata_t &inputdata ) +{ + m_bAllowedClasses[TF_CLASS_PYRO] = inputdata.value.Bool(); +} + +void CTFBotRoster::InputSetAllowSpy( inputdata_t &inputdata ) +{ + m_bAllowedClasses[TF_CLASS_SPY] = inputdata.value.Bool(); +} + +void CTFBotRoster::InputSetAllowEngineer( inputdata_t &inputdata ) +{ + m_bAllowedClasses[TF_CLASS_ENGINEER] = inputdata.value.Bool(); +} + +//------------------------------------------------------------------------------ + +bool CTFBotRoster::IsClassAllowed( int iBotClass ) const +{ + return iBotClass > TF_CLASS_UNDEFINED && iBotClass < TF_LAST_NORMAL_CLASS && m_bAllowedClasses[iBotClass]; +} + +//------------------------------------------------------------------------------ + +bool CTFBotRoster::IsClassChangeAllowed() const +{ + return m_bAllowClassChanges; +} diff --git a/game/server/tf/bot/map_entities/tf_bot_roster.h b/game/server/tf/bot/map_entities/tf_bot_roster.h new file mode 100644 index 0000000..e3931ba --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_bot_roster.h @@ -0,0 +1,39 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_roster.h +// entity that dictates what classes a bot can choose when spawning +// Tom Bui, April 2010 + +#ifndef TF_BOT_ROSTER_H +#define TF_BOT_ROSTER_H + +class CTFBotRoster : public CPointEntity +{ + DECLARE_CLASS( CTFBotRoster, CPointEntity ); +public: + DECLARE_DATADESC(); + + CTFBotRoster( void ); + virtual ~CTFBotRoster() {} + + // input + void InputSetAllowScout( inputdata_t &inputdata ); + void InputSetAllowSniper( inputdata_t &inputdata ); + void InputSetAllowSoldier( inputdata_t &inputdata ); + void InputSetAllowDemoman( inputdata_t &inputdata ); + void InputSetAllowMedic( inputdata_t &inputdata ); + void InputSetAllowHeavy( inputdata_t &inputdata ); + void InputSetAllowPyro( inputdata_t &inputdata ); + void InputSetAllowSpy( inputdata_t &inputdata ); + void InputSetAllowEngineer( inputdata_t &inputdata ); + + // misc. + bool IsClassAllowed( int iBotClass ) const; + bool IsClassChangeAllowed() const; + +public: + string_t m_teamName; + bool m_bAllowClassChanges; + bool m_bAllowedClasses[TF_LAST_NORMAL_CLASS]; +}; + +#endif // TF_BOT_ROSTER_H diff --git a/game/server/tf/bot/map_entities/tf_spawner.cpp b/game/server/tf/bot/map_entities/tf_spawner.cpp new file mode 100644 index 0000000..fa4f7c7 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_spawner.cpp @@ -0,0 +1,163 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_spawner.cpp +// Entity to spawn one or more templatized entities +// Michael Booth, April 2011 + +#include "cbase.h" + +#include "tf_gamerules.h" +#include "bot/map_entities/tf_spawner.h" + + +//------------------------------------------------------------------------------ +BEGIN_DATADESC( CTFSpawner ) + DEFINE_KEYFIELD( m_spawnCount, FIELD_INTEGER, "count" ), + DEFINE_KEYFIELD( m_maxActiveCount, FIELD_INTEGER, "maxActive" ), + DEFINE_KEYFIELD( m_spawnInterval, FIELD_FLOAT, "interval" ), + DEFINE_KEYFIELD( m_templateName, FIELD_STRING, "template" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Reset", InputReset ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + DEFINE_OUTPUT( m_onExpended, "OnExpended" ), + DEFINE_OUTPUT( m_onSpawned, "OnSpawned" ), + DEFINE_OUTPUT( m_onKilled, "OnKilled" ), + + DEFINE_THINKFUNC( SpawnerThink ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( tf_spawner, CTFSpawner ); + + +//------------------------------------------------------------------------------ +CTFSpawner::CTFSpawner( void ) +{ + Reset(); +} + + +//------------------------------------------------------------------------------ +void CTFSpawner::Reset( void ) +{ + m_bExpended = false; + m_spawnCountRemaining = 0; + m_spawnedVector.RemoveAll(); + SetThink( NULL ); +} + + +//------------------------------------------------------------------------------ +void CTFSpawner::InputReset( inputdata_t &inputdata ) +{ + Reset(); +} + + +//------------------------------------------------------------------------------ +void CTFSpawner::InputEnable( inputdata_t &inputdata ) +{ + if ( m_bExpended ) + { + return; + } + + SetThink( &CTFSpawner::SpawnerThink ); + + if ( m_spawnCountRemaining ) + { + // already generating - don't restart count + return; + } + + SetNextThink( gpGlobals->curtime ); + m_spawnCountRemaining = m_spawnCount; + + m_template = dynamic_cast< CTFSpawnTemplate * >( gEntList.FindEntityByName( NULL, m_templateName ) ); + if ( m_template == NULL ) + { + Warning( "%s failed to find template named '%s'\n", GetClassname(), STRING( m_templateName ) ); + } +} + + +//------------------------------------------------------------------------------ +void CTFSpawner::InputDisable( inputdata_t &inputdata ) +{ + // just stop thinking + SetThink( NULL ); +} + + +//------------------------------------------------------------------------------ +void CTFSpawner::OnKilled( CBaseEntity *dead ) +{ + m_onKilled.FireOutput( dead, this ); +} + + +//------------------------------------------------------------------------------ +void CTFSpawner::SpawnerThink( void ) +{ + // still waiting for the real game to start? + gamerules_roundstate_t roundState = TFGameRules()->State_Get(); + if ( roundState >= GR_STATE_TEAM_WIN || roundState < GR_STATE_PREROUND || TFGameRules()->IsInWaitingForPlayers() ) + { + SetNextThink( gpGlobals->curtime + 1.0f ); + return; + } + + // clean up destroyed children + for ( int i = 0; i < m_spawnedVector.Count(); ) + { + CHandle< CBaseEntity > child = m_spawnedVector[i]; + + if ( child == NULL ) + { + m_spawnedVector.FastRemove(i); + m_onKilled.FireOutput( this, this ); + continue; + } + + ++i; + } + + if ( m_spawnedVector.Count() >= m_maxActiveCount ) + { + // reached max simultanous active count + SetNextThink( gpGlobals->curtime + 0.1f ); + return; + } + + if ( m_template == NULL ) + { + // nothing to spawn! + return; + } + + // spawn the entity + CBaseEntity *child = m_template->Instantiate(); + if ( child ) + { + m_spawnedVector.AddToTail( child ); + + child->SetAbsOrigin( GetAbsOrigin() ); + child->SetAbsAngles( GetAbsAngles() ); + child->SetOwnerEntity( this ); + + DispatchSpawn( child ); + m_onSpawned.FireOutput( child, this ); + + --m_spawnCountRemaining; + if ( m_spawnCountRemaining ) + { + SetNextThink( gpGlobals->curtime + m_spawnInterval ); + } + else + { + SetThink( NULL ); + m_onExpended.FireOutput( this, this ); + m_bExpended = true; + } + } +} diff --git a/game/server/tf/bot/map_entities/tf_spawner.h b/game/server/tf/bot/map_entities/tf_spawner.h new file mode 100644 index 0000000..f57db89 --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_spawner.h @@ -0,0 +1,66 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_spawner.h +// Entity to spawn one or more templatized entities +// Michael Booth, April 2011 + +#ifndef TF_SPAWNER_H +#define TF_SPAWNER_H + +//-------------------------------------------------------- +/** + * Each particular type of entity the tf_spawner can create + * has an associated template (derived from this class) + * which defines its spawning location and initial properties. + */ +class CTFSpawnTemplate : public CPointEntity +{ +public: + DECLARE_CLASS( CTFSpawnTemplate, CPointEntity ); + + virtual ~CTFSpawnTemplate() { } + + virtual CBaseEntity *Instantiate( void ) const = 0; // spawn an instance of this template +}; + + +//-------------------------------------------------------- +class CTFSpawner : public CPointEntity +{ +public: + DECLARE_CLASS( CTFSpawner, CPointEntity ); + DECLARE_DATADESC(); + + CTFSpawner( void ); + virtual ~CTFSpawner() { } + + void SpawnerThink( void ); + + // Input. + void InputReset( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + // Output + void OnKilled( CBaseEntity *dead ); + +private: + void Reset( void ); + + bool m_bExpended; + int m_spawnCount; + int m_spawnCountRemaining; + int m_maxActiveCount; + float m_spawnInterval; + + string_t m_templateName; + CHandle< CTFSpawnTemplate > m_template; + + COutputEvent m_onSpawned; + COutputEvent m_onExpended; + COutputEvent m_onKilled; + + CUtlVector< CHandle< CBaseEntity > > m_spawnedVector; +}; + + +#endif // TF_SPAWNER_H diff --git a/game/server/tf/bot/map_entities/tf_spawner_boss.cpp b/game/server/tf/bot/map_entities/tf_spawner_boss.cpp new file mode 100644 index 0000000..4381dac --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_spawner_boss.cpp @@ -0,0 +1,167 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_spawner_boss.cpp +// Entity to spawn a Boss +// Michael Booth, February 2011 + +#include "cbase.h" + +#ifdef OBSOLETE_USE_BOSS_ALPHA + +#ifdef TF_RAID_MODE + +#include "tf_gamerules.h" +#include "tf_spawner_boss.h" +#include "bot_npc/bot_npc.h" + + +//------------------------------------------------------------------------------ + +BEGIN_DATADESC( CTFSpawnerBoss ) + DEFINE_KEYFIELD( m_spawnCount, FIELD_INTEGER, "count" ), + DEFINE_KEYFIELD( m_maxActiveCount, FIELD_INTEGER, "maxActive" ), + DEFINE_KEYFIELD( m_spawnInterval, FIELD_FLOAT, "interval" ), + DEFINE_KEYFIELD( m_teamName, FIELD_STRING, "team" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + DEFINE_OUTPUT( m_onSpawned, "OnSpawned" ), + DEFINE_OUTPUT( m_onExpended, "OnExpended" ), + DEFINE_OUTPUT( m_onBotKilled, "OnBotKilled" ), + DEFINE_OUTPUT( m_onBotStunned, "OnBotStunned" ), + + DEFINE_THINKFUNC( SpawnerThink ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( tf_spawner_boss, CTFSpawnerBoss ); + + +//------------------------------------------------------------------------------ +CTFSpawnerBoss::CTFSpawnerBoss( void ) +{ + m_isExpended = false; + m_spawnCountRemaining = 0; + + SetThink( NULL ); +} + +//------------------------------------------------------------------------------ +void CTFSpawnerBoss::InputEnable( inputdata_t &inputdata ) +{ + if ( m_isExpended ) + { + return; + } + + SetThink( &CTFSpawnerBoss::SpawnerThink ); + + if ( m_spawnCountRemaining ) + { + // already generating - don't restart count + return; + } + SetNextThink( gpGlobals->curtime ); + m_spawnCountRemaining = m_spawnCount; +} + + +//------------------------------------------------------------------------------ +void CTFSpawnerBoss::InputDisable( inputdata_t &inputdata ) +{ + // just stop thinking + SetThink( NULL ); +} + + +//------------------------------------------------------------------------------ +void CTFSpawnerBoss::OnBotKilled( CBotNPC *pBot ) +{ + m_onBotKilled.FireOutput( pBot, this ); +} + + +//------------------------------------------------------------------------------ +void CTFSpawnerBoss::OnBotStunned( CBotNPC *pBot ) +{ + m_onBotStunned.FireOutput( pBot, this ); +} + + +//------------------------------------------------------------------------------ +void CTFSpawnerBoss::SpawnerThink( void ) +{ + // still waiting for the real game to start? + gamerules_roundstate_t roundState = TFGameRules()->State_Get(); + if ( roundState >= GR_STATE_TEAM_WIN || roundState < GR_STATE_PREROUND || TFGameRules()->IsInWaitingForPlayers() ) + { + SetNextThink( gpGlobals->curtime + 1.0f ); + return; + } + + // remove invalid handles from our collection + int i = 0; + while( i < m_spawnedBotVector.Count() ) + { + CHandle< CBotNPC > hBot = m_spawnedBotVector[i]; + if ( hBot == NULL ) + { + m_spawnedBotVector.FastRemove(i); + continue; + } + + ++i; + } + + if ( m_spawnedBotVector.Count() >= m_maxActiveCount ) + { + // maximum count reached - can't spawn any more + SetNextThink( gpGlobals->curtime + 0.1f ); + return; + } + + // spawn a bot + CBotNPC *bot = (CBotNPC *)CreateEntityByName( "bot_boss" ); + if ( bot ) + { + m_spawnedBotVector.AddToTail( bot ); + + int iTeam = TEAM_UNASSIGNED; + if ( FStrEq( m_teamName.ToCStr(), "red" ) ) + { + iTeam = TF_TEAM_RED; + } + else if ( FStrEq( m_teamName.ToCStr(), "blue" ) ) + { + iTeam = TF_TEAM_BLUE; + } + bot->ChangeTeam( iTeam ); + + // match bot facing to that of spawner + bot->SetAbsAngles( GetAbsAngles() ); + + bot->SetAbsOrigin( GetAbsOrigin() ); + + bot->SetSpawner( this ); + + DispatchSpawn( bot ); + + m_onSpawned.FireOutput( bot, this ); + + --m_spawnCountRemaining; + if ( m_spawnCountRemaining ) + { + SetNextThink( gpGlobals->curtime + m_spawnInterval ); + } + else + { + SetThink( NULL ); + m_onExpended.FireOutput( this, this ); + m_isExpended = true; + } + } +} + +#endif // TF_RAID_MODE + +#endif // #ifdef OBSOLETE_USE_BOSS_ALPHA + diff --git a/game/server/tf/bot/map_entities/tf_spawner_boss.h b/game/server/tf/bot/map_entities/tf_spawner_boss.h new file mode 100644 index 0000000..13f247a --- /dev/null +++ b/game/server/tf/bot/map_entities/tf_spawner_boss.h @@ -0,0 +1,54 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_spawner_boss.h +// Entity to spawn a Boss +// Michael Booth, February 2011 + +#ifndef TF_SPAWNER_BOSS_H +#define TF_SPAWNER_BOSS_H + +#ifdef OBSOLETE_USE_BOSS_ALPHA + +#ifdef TF_RAID_MODE + +class CBotNPC; + +class CTFSpawnerBoss : public CPointEntity +{ +public: + DECLARE_CLASS( CTFSpawnerBoss, CPointEntity ); + DECLARE_DATADESC(); + + CTFSpawnerBoss( void ); + virtual ~CTFSpawnerBoss() { } + + void SpawnerThink( void ); + + // Input. + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + // Output + void OnBotKilled( CBotNPC *pBot ); + void OnBotStunned( CBotNPC *pBot ); + +private: + bool m_isExpended; + int m_spawnCount; + int m_spawnCountRemaining; + int m_maxActiveCount; + float m_spawnInterval; + string_t m_teamName; + + COutputEvent m_onSpawned; + COutputEvent m_onExpended; + COutputEvent m_onBotKilled; + COutputEvent m_onBotStunned; + + CUtlVector< CHandle< CBotNPC > > m_spawnedBotVector; +}; + +#endif // TF_RAID_MODE + +#endif // OBSOLETE_USE_BOSS_ALPHA + +#endif // TF_SPAWNER_BOSS_H diff --git a/game/server/tf/bot/tf_bot.cpp b/game/server/tf/bot/tf_bot.cpp new file mode 100644 index 0000000..6bafab4 --- /dev/null +++ b/game/server/tf/bot/tf_bot.cpp @@ -0,0 +1,4644 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot.cpp +// Team Fortress NextBot +// Michael Booth, February 2009 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_obj_sentrygun.h" +#include "team_control_point_master.h" +#include "tf_weapon_pipebomblauncher.h" +#include "team_train_watcher.h" +#include "tf_bot.h" +#include "tf_bot_manager.h" +#include "tf_bot_vision.h" +#include "tf_team.h" +#include "bot/map_entities/tf_bot_generator.h" +#include "trigger_area_capture.h" +#include "GameEventListener.h" +#include "NextBotUtil.h" +#include "tier3/tier3.h" +#include "vgui/ILocalize.h" +#include "econ_item_system.h" +#include "bot/behavior/tf_bot_use_item.h" +#include "tf_wearable_item_demoshield.h" +#include "tf_weapon_buff_item.h" +#include "tf_weapon_lunchbox.h" +#include "func_respawnroom.h" +#include "soundenvelope.h" + +#include "econ_entity_creation.h" + +#include "player_vs_environment/tf_population_manager.h" + +#include "bot/behavior/tf_bot_behavior.h" +#include "bot/map_entities/tf_bot_generator.h" +#include "bot/map_entities/tf_bot_hint_entity.h" + +ConVar tf_bot_force_class( "tf_bot_force_class", "", FCVAR_GAMEDLL, "If set to a class name, all TFBots will respawn as that class" ); + +ConVar tf_bot_notice_gunfire_range( "tf_bot_notice_gunfire_range", "3000", FCVAR_GAMEDLL ); +ConVar tf_bot_notice_quiet_gunfire_range( "tf_bot_notice_quiet_gunfire_range", "500", FCVAR_GAMEDLL ); +ConVar tf_bot_sniper_personal_space_range( "tf_bot_sniper_personal_space_range", "1000", FCVAR_CHEAT, "Enemies beyond this range don't worry the Sniper" ); +ConVar tf_bot_pyro_deflect_tolerance( "tf_bot_pyro_deflect_tolerance", "0.5", FCVAR_CHEAT ); +ConVar tf_bot_keep_class_after_death( "tf_bot_keep_class_after_death", "0", FCVAR_GAMEDLL ); +ConVar tf_bot_prefix_name_with_difficulty( "tf_bot_prefix_name_with_difficulty", "0", FCVAR_GAMEDLL, "Append the skill level of the bot to the bot's name" ); +ConVar tf_bot_near_point_travel_distance( "tf_bot_near_point_travel_distance", "750", FCVAR_CHEAT, "If within this travel distance to the current point, bot is 'near' it" ); +ConVar tf_bot_pyro_shove_away_range( "tf_bot_pyro_shove_away_range", "250", FCVAR_CHEAT, "If a Pyro bot's target is closer than this, compression blast them away" ); +ConVar tf_bot_pyro_always_reflect( "tf_bot_pyro_always_reflect", "0", FCVAR_CHEAT, "Pyro bots will always reflect projectiles fired at them. For tesing/debugging purposes." ); + +ConVar tf_bot_sniper_spot_min_range( "tf_bot_sniper_spot_min_range", "1000", FCVAR_CHEAT ); +ConVar tf_bot_sniper_spot_max_count( "tf_bot_sniper_spot_max_count", "10", FCVAR_CHEAT, "Stop searching for sniper spots when each side has found this many" ); +ConVar tf_bot_sniper_spot_search_count( "tf_bot_sniper_spot_search_count", "10", FCVAR_CHEAT, "Search this many times per behavior update frame" ); +ConVar tf_bot_sniper_spot_point_tolerance( "tf_bot_sniper_spot_point_tolerance", "750", FCVAR_CHEAT ); +ConVar tf_bot_sniper_spot_epsilon( "tf_bot_sniper_spot_epsilon", "100", FCVAR_CHEAT ); + +ConVar tf_bot_sniper_goal_entity_move_tolerance( "tf_bot_sniper_goal_entity_move_tolerance", "500", FCVAR_CHEAT ); + +ConVar tf_bot_suspect_spy_touch_interval( "tf_bot_suspect_spy_touch_interval", "5", FCVAR_CHEAT, "How many seconds back to look for touches against suspicious spies" ); +ConVar tf_bot_suspect_spy_forget_cooldown( "tf_bot_suspect_spy_forget_cooldown", "5", FCVAR_CHEAT, "How long to consider a suspicious spy as suspicious" ); + +ConVar tf_bot_debug_tags( "tf_bot_debug_tags", "0", FCVAR_CHEAT, "ent_text will only show tags on bots" ); + +extern ConVar tf_bot_sniper_spot_max_count; +extern ConVar tf_bot_fire_weapon_min_time; +extern ConVar tf_bot_sniper_misfire_chance; +extern ConVar tf_bot_difficulty; +extern ConVar tf_bot_farthest_visible_theater_sample_count; +extern ConVar tf_bot_sniper_spot_min_range; +extern ConVar tf_bot_sniper_spot_epsilon; +extern ConVar tf_mvm_miniboss_min_health; +extern ConVar tf_bot_path_lookahead_range; + +extern ConVar tf_mvm_miniboss_scale; + + +//----------------------------------------------------------------------------------------------------- +bool IsPlayerClassname( const char *string ) +{ + for ( int i = TF_CLASS_SCOUT; i < TF_CLASS_COUNT_ALL; ++i ) + { + if ( !stricmp( string, GetPlayerClassData( i )->m_szClassName ) ) + { + return true; + } + } + + return false; +} + + +//----------------------------------------------------------------------------------------------------- +bool IsTeamName( const char *string ) +{ + if ( !stricmp( string, "red" ) ) + return true; + + if ( !stricmp( string, "blue" ) ) + return true; + + return false; +} + + +//----------------------------------------------------------------------------------------------------- +CTFBot::DifficultyType StringToDifficultyLevel( const char *string ) +{ + if ( !stricmp( string, "easy" ) ) + return CTFBot::EASY; + + if ( !stricmp( string, "normal" ) ) + return CTFBot::NORMAL; + + if ( !stricmp( string, "hard" ) ) + return CTFBot::HARD; + + if ( !stricmp( string, "expert" ) ) + return CTFBot::EXPERT; + + return CTFBot::UNDEFINED; +} + + +//----------------------------------------------------------------------------------------------------- +const char *DifficultyLevelToString( CTFBot::DifficultyType skill ) +{ + switch( skill ) + { + case CTFBot::EASY: return "Easy "; + case CTFBot::NORMAL: return "Normal "; + case CTFBot::HARD: return "Hard "; + case CTFBot::EXPERT: return "Expert "; + } + + return "Undefined "; +} + + +//----------------------------------------------------------------------------------------------------- +const char *GetRandomBotName( void ) +{ + static const char *nameList[] = + { + "Chucklenuts", + "CryBaby", + "WITCH", + "ThatGuy", + "Still Alive", + "Hat-Wearing MAN", + "Me", + "Numnutz", + "H@XX0RZ", + "The G-Man", + "Chell", + "The Combine", + "Totally Not A Bot", + "Pow!", + "Zepheniah Mann", + "THEM", + "LOS LOS LOS", + "10001011101", + "DeadHead", + "ZAWMBEEZ", + "MindlessElectrons", + "TAAAAANK!", + "The Freeman", + "Black Mesa", + "Soulless", + "CEDA", + "BeepBeepBoop", + "NotMe", + "CreditToTeam", + "BoomerBile", + "Someone Else", + "Mann Co.", + "Dog", + "Kaboom!", + "AmNot", + "0xDEADBEEF", + "HI THERE", + "SomeDude", + "GLaDOS", + "Hostage", + "Headful of Eyeballs", + "CrySomeMore", + "Aperture Science Prototype XR7", + "Humans Are Weak", + "AimBot", + "C++", + "GutsAndGlory!", + "Nobody", + "Saxton Hale", + "RageQuit", + "Screamin' Eagles", + + "Ze Ubermensch", + "Maggot", + "CRITRAWKETS", + "Herr Doktor", + "Gentlemanne of Leisure", + "Companion Cube", + "Target Practice", + "One-Man Cheeseburger Apocalypse", + "Crowbar", + "Delicious Cake", + "IvanTheSpaceBiker", + "I LIVE!", + "Cannon Fodder", + + "trigger_hurt", + "Nom Nom Nom", + "Divide by Zero", + "GENTLE MANNE of LEISURE", + "MoreGun", + "Tiny Baby Man", + "Big Mean Muther Hubbard", + "Force of Nature", + + "Crazed Gunman", + "Grim Bloody Fable", + "Poopy Joe", + "A Professional With Standards", + "Freakin' Unbelievable", + "SMELLY UNFORTUNATE", + "The Administrator", + "Mentlegen", + + "Archimedes!", + "Ribs Grow Back", + "It's Filthy in There!", + "Mega Baboon", + "Kill Me", + "Glorified Toaster with Legs", + +#ifdef STAGING_ONLY + "John Spartan", + "Leeloo Dallas Multipass", + "Sho'nuff", + "Bruce Leroy", + "CAN YOUUUUUUUUU DIG IT?!?!?!?!", + "Big Gulp, Huh?", + "Stupid Hot Dog", + "I'm your huckleberry", + "The Crocketeer", +#endif + NULL + }; + static int nameCount = 0; + static int nameIndex = 0; + + if ( nameCount == 0 ) + { + for( ; nameList[ nameCount ]; ++nameCount ); + + // randomize the initial index + nameIndex = RandomInt( 0, nameCount-1 ); + } + + const char *name = nameList[ nameIndex++ ]; + + if ( nameIndex >= nameCount ) + nameIndex = 0; + + return name; +} + + +//----------------------------------------------------------------------------------------------------- +void CreateBotName( int iTeam, int iClassIndex, CTFBot::DifficultyType skill, char* pBuffer, int iBufferSize ) +{ + char szBotNameBuffer[256]; + char szEnemyOrFriendlyString[256]; + + const char *pBotName = ""; + const char *pFriendlyOrEnemyTitle = ""; + + // @note (Tom Bui): it is okay to get localized name in training, since we should be on a listen server + if ( TFGameRules()->IsInTraining() ) + { + // get the friendly/enemy title + const char *pBotTitle = NULL; + if ( iTeam != TEAM_UNASSIGNED ) + { + int iHumanTeam = TFGameRules()->GetAssignedHumanTeam(); + if ( iHumanTeam != TEAM_ANY ) + { + if ( iHumanTeam == iTeam ) + { + pBotTitle = "#TF_Bot_Title_Friendly"; + } + else + { + pBotTitle = "#TF_Bot_Title_Enemy"; + } + } + } + wchar_t *pLocalizedTitle = pBotTitle ? g_pVGuiLocalize->Find( pBotTitle ) : NULL; + if ( pLocalizedTitle ) + { + g_pVGuiLocalize->ConvertUnicodeToANSI( pLocalizedTitle, szEnemyOrFriendlyString, sizeof( szEnemyOrFriendlyString ) ); + pFriendlyOrEnemyTitle = szEnemyOrFriendlyString; + } + + // get the class name + wchar_t *pLocalizedName = NULL; + if ( iClassIndex >= TF_FIRST_NORMAL_CLASS && iClassIndex < TF_LAST_NORMAL_CLASS ) + { + pLocalizedName = g_pVGuiLocalize->Find( g_aPlayerClassNames[ iClassIndex ] ); + } + else + { + pLocalizedName = g_pVGuiLocalize->Find( "#TF_Bot_Generic_ClassName" ); + } + g_pVGuiLocalize->ConvertUnicodeToANSI( pLocalizedName, szBotNameBuffer, sizeof( szBotNameBuffer ) ); + pBotName = szBotNameBuffer; + } + else + { + pBotName = GetRandomBotName(); + } + + const char *pDifficultyString = tf_bot_prefix_name_with_difficulty.GetBool() ? DifficultyLevelToString( skill ) : ""; + + // we use this as our formatting, because we don't know the language of the downstream clients + CFmtStr name( "%s%s%s", + pDifficultyString, pFriendlyOrEnemyTitle, pBotName ); + Q_strncpy( pBuffer, name.Access(), iBufferSize ); +} + + +//----------------------------------------------------------------------------------------------------- +CON_COMMAND_F( tf_bot_add, "Add a bot.", FCVAR_GAMEDLL ) +{ + // Listenserver host or rcon access only! + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + bool bQuotaManaged = true; + int botCount = 1; + const char *classname = NULL; + const char *teamname = "auto"; + const char *pszBotNameViaArg = NULL; + CTFBot::DifficultyType skill = clamp( (CTFBot::DifficultyType)tf_bot_difficulty.GetInt(), CTFBot::EASY, CTFBot::EXPERT ); + + int i; + for( i=1; i<args.ArgC(); ++i ) + { + CTFBot::DifficultyType trySkill = StringToDifficultyLevel( args.Arg(i) ); + int nArgAsInteger = atoi( args.Arg(i) ); + + // each argument could be a classname, a team, a difficulty level, a count, or a name + if ( IsPlayerClassname( args.Arg(i) ) ) + { + classname = args.Arg(i); + } + else if ( IsTeamName( args.Arg(i) ) ) + { + teamname = args.Arg(i); + } + else if ( !stricmp( args.Arg( i ), "noquota" ) ) + { + bQuotaManaged = false; + } + else if ( trySkill != CTFBot::UNDEFINED ) + { + skill = trySkill; + } + else if ( nArgAsInteger > 0 ) + { + botCount = nArgAsInteger; + pszBotNameViaArg = NULL; // can't have a custom name if spawning multiple bots + } + else if ( botCount == 1 ) + { + pszBotNameViaArg = args.Arg( i ); + } + else + { + Warning( "Invalid argument '%s'\n", args.Arg(i) ); + } + } + + // cvar can override classname + classname = FStrEq( tf_bot_force_class.GetString(), "" ) ? classname : tf_bot_force_class.GetString(); + int iClassIndex = classname ? GetClassIndexFromString( classname ) : TF_CLASS_UNDEFINED; + + int iTeam = TEAM_UNASSIGNED; + if ( FStrEq( teamname, "red" ) ) + { + iTeam = TF_TEAM_RED; + } + else if ( FStrEq( teamname, "blue" ) ) + { + iTeam = TF_TEAM_BLUE; + } + + if ( TFGameRules()->IsInTraining() ) + { + skill = CTFBot::EASY; + } + + char name[256]; + int iNumAdded = 0; + for( i=0; i<botCount; ++i ) + { + CTFBot *pBot = NULL; + const char *pszBotName = NULL; + + if ( !pszBotNameViaArg ) + { + CreateBotName( iTeam, iClassIndex, skill, name, sizeof(name) ); + pszBotName = name; + } + else + { + pszBotName = pszBotNameViaArg; + } + + pBot = NextBotCreatePlayerBot< CTFBot >( pszBotName ); + + if ( pBot ) + { + if ( bQuotaManaged ) + { + pBot->SetAttribute( CTFBot::QUOTA_MANANGED ); + } + + pBot->HandleCommand_JoinTeam( teamname ); + + pBot->SetDifficulty( skill ); + + // if no class is set, auto-select one + const char *thisClassname = classname ? classname : pBot->GetNextSpawnClassname(); + pBot->HandleCommand_JoinClass( thisClassname ); + + // set up a proper name now that we are in training + if ( TFGameRules()->IsInTraining() ) + { + CreateBotName( pBot->GetTeamNumber(), pBot->GetPlayerClass()->GetClassIndex(), skill, name, sizeof(name) ); + engine->SetFakeClientConVarValue( pBot->edict(), "name", name ); + } + + ++iNumAdded; + } + } + + if ( bQuotaManaged ) + { + TheTFBots().OnForceAddedBots( iNumAdded ); + } +} + + +//----------------------------------------------------------------------------------------------------- +CON_COMMAND_F( tf_bot_kick, "Remove a TFBot by name, or all bots (\"all\").", FCVAR_GAMEDLL ) +{ + // Listenserver host or rcon access only! + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( args.ArgC() < 2 ) + { + DevMsg( "%s <bot name>, \"red\", \"blue\", or \"all\"> <optional: \"moveToSpectatorTeam\"> \n", args.Arg(0) ); + return; + } + + bool bMoveToSpectatorTeam = false; + int iTeam = TEAM_UNASSIGNED; + int i; + const char *pPlayerName = ""; + for( i=1; i<args.ArgC(); ++i ) + { + // each argument could be a classname, a team, or a count + if ( FStrEq( args.Arg(i), "red" ) ) + { + iTeam = TF_TEAM_RED; + } + else if ( FStrEq( args.Arg(i), "blue" ) ) + { + iTeam = TF_TEAM_BLUE; + } + else if ( FStrEq( args.Arg(i), "all" ) ) + { + iTeam = TEAM_ANY; + } + else if ( FStrEq( args.Arg(i), "moveToSpectatorTeam" ) ) + { + bMoveToSpectatorTeam = true; + } + else + { + pPlayerName = args.Arg(i); + } + } + + int iNumKicked = 0; + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if ( !player ) + continue; + + if ( FNullEnt( player->edict() ) ) + continue; + + if ( player->MyNextBotPointer() ) + { + if ( iTeam == TEAM_ANY || + FStrEq( pPlayerName, player->GetPlayerName() ) || + ( player->GetTeamNumber() == iTeam ) || + ( player->GetTeamNumber() == iTeam ) ) + { + if ( bMoveToSpectatorTeam ) + { + player->ChangeTeam( TEAM_SPECTATOR, false, true ); + } + else + { + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", player->GetUserID() ) ); + } + CTFBot* pBot = dynamic_cast< CTFBot* >( player ); + if ( pBot && pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) ) + { + ++iNumKicked; + } + } + } + } + TheTFBots().OnForceKickedBots( iNumKicked ); +} + + +//----------------------------------------------------------------------------------------------------- +CON_COMMAND_F( tf_bot_kill, "Kill a TFBot by name, or all bots (\"all\").", FCVAR_GAMEDLL ) +{ + // Listenserver host or rcon access only! + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( args.ArgC() < 2 ) + { + DevMsg( "%s <bot name>, \"red\", \"blue\", or \"all\"> <optional: \"moveToSpectatorTeam\"> \n", args.Arg(0) ); + return; + } + + int iTeam = TEAM_UNASSIGNED; + int i; + const char *pPlayerName = ""; + for( i=1; i<args.ArgC(); ++i ) + { + // each argument could be a classname, a team, or a count + if ( FStrEq( args.Arg(i), "red" ) ) + { + iTeam = TF_TEAM_RED; + } + else if ( FStrEq( args.Arg(i), "blue" ) ) + { + iTeam = TF_TEAM_BLUE; + } + else if ( FStrEq( args.Arg(i), "all" ) ) + { + iTeam = TEAM_ANY; + } + else if ( FStrEq( args.Arg(i), "moveToSpectatorTeam" ) ) + { + // bMoveToSpectatorTeam = true; + } + else + { + pPlayerName = args.Arg(i); + } + } + + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if ( !player ) + continue; + + if ( FNullEnt( player->edict() ) ) + continue; + + if ( player->MyNextBotPointer() ) + { + if ( iTeam == TEAM_ANY || + FStrEq( pPlayerName, player->GetPlayerName() ) || + ( player->GetTeamNumber() == iTeam ) || + ( player->GetTeamNumber() == iTeam ) ) + { + CTakeDamageInfo info( player, player, 9999999.9f, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE ); + player->TakeDamage( info ); + } + } + } +} + +//----------------------------------------------------------------------------------------------------- +void CMD_BotWarpTeamToMe( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( !player ) + return; + + CTeam *myTeam = player->GetTeam(); + for( int i=0; i<myTeam->GetNumPlayers(); ++i ) + { + if ( !myTeam->GetPlayer(i)->IsAlive() ) + continue; + + myTeam->GetPlayer(i)->SetAbsOrigin( player->GetAbsOrigin() ); + } +} +static ConCommand tf_bot_warp_team_to_me( "tf_bot_warp_team_to_me", CMD_BotWarpTeamToMe, "", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//----------------------------------------------------------------------------------------------------- +IMPLEMENT_INTENTION_INTERFACE( CTFBot, CTFBotMainAction ); + + +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( tf_bot, CTFBot ); + + +//----------------------------------------------------------------------------------------------------- +/** + * Allocate a bot and bind it to the edict + */ +CBasePlayer *CTFBot::AllocatePlayerEntity( edict_t *edict, const char *playerName ) +{ + CBasePlayer::s_PlayerEdict = edict; + return static_cast< CBasePlayer * >( CreateEntityByName( "tf_bot" ) ); +} + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::PressFireButton( float duration ) +{ + // can't fire if stunned + // @todo Tom Bui: Eventually, we'll probably want to check the actual weapon for supress fire + if ( m_Shared.IsControlStunned() || m_Shared.IsLoserStateStunned() || HasAttribute( CTFBot::SUPPRESS_FIRE ) ) + { + ReleaseFireButton(); + return; + } + + BaseClass::PressFireButton( duration ); +} + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::PressAltFireButton( float duration ) +{ + // can't fire if stunned + // @todo Tom Bui: Eventually, we'll probably want to check the actual weapon for supress fire + if ( m_Shared.IsControlStunned() || m_Shared.IsLoserStateStunned() || HasAttribute( CTFBot::SUPPRESS_FIRE ) ) + { + ReleaseAltFireButton(); + return; + } + + BaseClass::PressAltFireButton( duration ); +} + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::PressSpecialFireButton( float duration ) +{ + // can't fire if stunned + // @todo Tom Bui: Eventually, we'll probably want to check the actual weapon for supress fire + if ( m_Shared.IsControlStunned() || m_Shared.IsLoserStateStunned() || HasAttribute( CTFBot::SUPPRESS_FIRE ) ) + { + ReleaseAltFireButton(); + return; + } + + BaseClass::PressSpecialFireButton( duration ); +} + + +//----------------------------------------------------------------------------------------------------- +class CCountClassMembers +{ +public: + CCountClassMembers( const CTFBot *me, int teamID ) + { + m_me = me; + m_myTeam = teamID; + m_teamSize = 0; + + for( int i=0; i<TF_LAST_NORMAL_CLASS; ++i ) + m_count[i] = 0; + } + + bool operator() ( CBasePlayer *basePlayer ) + { + CTFPlayer *player = (CTFPlayer *)basePlayer; + + if ( player->GetTeamNumber() != m_myTeam ) + return true; + + ++m_teamSize; + + if ( m_me->IsSelf( player ) ) + return true; + + ++m_count[ player->GetDesiredPlayerClassIndex() ]; + + return true; + } + + const CTFBot *m_me; + int m_myTeam; + int m_count[ TF_LAST_NORMAL_CLASS+1 ]; + int m_teamSize; +}; + + +//----------------------------------------------------------------------------------------------------- +/** + * NOTE: Assumes bot's difficulty has been set, and the bot is on a team. + */ +const char *CTFBot::GetNextSpawnClassname( void ) const +{ + struct ClassSelectionInfo + { + int m_class; + int m_minTeamSizeToSelect; // team must have this many members to choose this class + int m_countPerTeamSize; // must have 1 Medic for each 4 team members, for example + int m_minLimit; // minimum that must be present (once other constraints are met) + int m_maxLimit[ NUM_DIFFICULTY_LEVELS ]; // maximum that can be present (-1 for infinite) + }; + + const int NoLimit = -1; + + static ClassSelectionInfo defenseRoster[] = + { + { TF_CLASS_ENGINEER, 0, 4, 1, { 1, 2, 3, 3 } }, + { TF_CLASS_SOLDIER, 0, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } }, + { TF_CLASS_DEMOMAN, 0, 0, 0, { 2, 3, 3, 3 } }, + { TF_CLASS_PYRO, 3, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } }, + { TF_CLASS_HEAVYWEAPONS, 3, 0, 0, { 1, 1, 2, 2 } }, + { TF_CLASS_MEDIC, 4, 4, 1, { 1, 1, 2, 2 } }, + { TF_CLASS_SNIPER, 5, 0, 0, { 0, 1, 1, 1 } }, + { TF_CLASS_SPY, 5, 0, 0, { 0, 1, 2, 2 } }, + + { TF_CLASS_UNDEFINED, 0, -1 }, + }; + + static ClassSelectionInfo offenseRoster[] = + { + { TF_CLASS_SCOUT, 0, 0, 1, { 3, 3, 3, 3 } }, + { TF_CLASS_SOLDIER, 0, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } }, + { TF_CLASS_DEMOMAN, 0, 0, 0, { 2, 3, 3, 3 } }, // must limit demomen, or the whole team will go demo to take out tough sentryguns + { TF_CLASS_PYRO, 3, 0, 0, { NoLimit, NoLimit, NoLimit, NoLimit } }, + { TF_CLASS_HEAVYWEAPONS, 3, 0, 0, { 1, 1, 2, 2 } }, + { TF_CLASS_MEDIC, 4, 4, 1, { 1, 1, 2, 2 } }, + { TF_CLASS_SNIPER, 5, 0, 0, { 0, 1, 1, 1 } }, + { TF_CLASS_SPY, 5, 0, 0, { 0, 1, 2, 2 } }, + { TF_CLASS_ENGINEER, 5, 0, 0, { 1, 1, 1, 1 } }, + + { TF_CLASS_UNDEFINED, 0, -1 }, + }; + + static ClassSelectionInfo compRoster[] = + { + { TF_CLASS_SCOUT, 0, 0, 0, { 0, 0, 2, 2 } }, + { TF_CLASS_SOLDIER, 0, 0, 0, { 0, 0, NoLimit, NoLimit } }, + { TF_CLASS_DEMOMAN, 0, 0, 0, { 0, 0, 2, 2 } }, // must limit demomen, or the whole team will go demo to take out tough sentryguns + { TF_CLASS_PYRO, 0, -1 }, + { TF_CLASS_HEAVYWEAPONS, 3, 0, 0, { 0, 0, 2, 2 } }, + { TF_CLASS_MEDIC, 1, 0, 1, { 0, 0, 1, 1 } }, + { TF_CLASS_SNIPER, 0, -1 }, + { TF_CLASS_SPY, 0, -1 }, + { TF_CLASS_ENGINEER, 0, -1 }, + + { TF_CLASS_UNDEFINED, 0, -1 }, + }; + + // if we are an engineer with an active sentry or teleporters, don't switch + if ( IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + if ( const_cast< CTFBot * >( this )->GetObjectOfType( OBJ_SENTRYGUN ) || + const_cast< CTFBot * >( this )->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT ) ) + { + return "engineer"; + } + } + + // count classes in use by my team, not including me + CCountClassMembers currentRoster( this, GetTeamNumber() ); + ForEachPlayer( currentRoster ); + + // assume offense + ClassSelectionInfo *desiredRoster = offenseRoster; + + if ( TFGameRules()->IsMatchTypeCompetitive() ) + { + desiredRoster = compRoster; + } + else if ( TFGameRules()->IsInKothMode() ) + { + CTeamControlPoint *point = GetMyControlPoint(); + if ( point ) + { + if ( GetTeamNumber() == ObjectiveResource()->GetOwningTeam( point->GetPointIndex() ) ) + { + // defend our point + desiredRoster = defenseRoster; + } + } + } + else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP ) + { + CUtlVector< CTeamControlPoint * > captureVector; + TFGameRules()->CollectCapturePoints( const_cast< CTFBot * >( this ), &captureVector ); + + CUtlVector< CTeamControlPoint * > defendVector; + TFGameRules()->CollectDefendPoints( const_cast< CTFBot * >( this ), &defendVector ); + + // if we have any points we can capture, try to do so + if ( captureVector.Count() > 0 || defendVector.Count() == 0 ) + { + desiredRoster = offenseRoster; + } + else + { + desiredRoster = defenseRoster; + } + } + else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT ) + { + if ( GetTeamNumber() == TF_TEAM_RED ) + { + desiredRoster = defenseRoster; + } + } + + // build vector of classes we can pick from + CUtlVector< int > desiredClassVector; + CUtlVector< int > allowedClassForBotRosterVector; + + for( int i=0; desiredRoster[ i ].m_class != TF_CLASS_UNDEFINED; ++i ) + { + ClassSelectionInfo *desiredClassInfo = &desiredRoster[ i ]; + + if ( TFGameRules()->CanBotChooseClass( const_cast< CTFBot * >( this ), desiredClassInfo->m_class ) == false ) + { + // not allowed to use this class + continue; + } + // just in case we hit the class limits, we want to make sure we select a class that is allowed + allowedClassForBotRosterVector.AddToTail( desiredClassInfo->m_class ); + + if ( currentRoster.m_teamSize < desiredClassInfo->m_minTeamSizeToSelect ) + { + // team is too small to choose this class + continue; + } + + // check limits + if ( currentRoster.m_count[ desiredClassInfo->m_class ] < desiredClassInfo->m_minLimit ) + { + // below required limit - choose only this class + desiredClassVector.RemoveAll(); + desiredClassVector.AddToTail( desiredClassInfo->m_class ); + break; + } + + int maxLimit = desiredClassInfo->m_maxLimit[ (int)clamp( GetDifficulty(), CTFBot::EASY, CTFBot::EXPERT ) ]; + + if ( maxLimit > NoLimit && currentRoster.m_count[ desiredClassInfo->m_class ] >= maxLimit ) + { + // at or above limit for this class + continue; + } + + if ( desiredClassInfo->m_countPerTeamSize > 0 ) + { + // how many of this class should there be at the given "per" count + int maxCountPer = currentRoster.m_teamSize / desiredClassInfo->m_countPerTeamSize; + if ( currentRoster.m_count[ desiredClassInfo->m_class ] - desiredClassInfo->m_minTeamSizeToSelect < maxCountPer ) + { + // below required limit - choose only this class + desiredClassVector.RemoveAll(); + desiredClassVector.AddToTail( desiredClassInfo->m_class ); + break; + } + } + + // valid class to choose + desiredClassVector.AddToTail( desiredClassInfo->m_class ); + } + + if ( desiredClassVector.Count() == 0 ) + { + if ( allowedClassForBotRosterVector.Count() == 0 ) + { + // nothing available + Warning( "TFBot unable to choose a class, defaulting to 'auto'\n" ); + return "auto"; + } + else + { + desiredClassVector = allowedClassForBotRosterVector; + } + } + + int which = RandomInt( 0, desiredClassVector.Count()-1 ); + + // if we need to destroy a sentry, pick a class that can do so + if ( GetEnemySentry() ) + { + // best sentry demolitions + int demoman = desiredClassVector.Find( TF_CLASS_DEMOMAN ); + if ( demoman >= 0 ) + { + which = demoman; + } + else + { + // next best sentry demolitions + int spy = desiredClassVector.Find( TF_CLASS_SPY ); + if ( spy >= 0 ) + { + which = spy; + } + else + { + // good sentry demolitions + int soldier = desiredClassVector.Find( TF_CLASS_SOLDIER ); + if ( soldier >= 0 ) + { + which = soldier; + } + } + } + } + + TFPlayerClassData_t *classData = GetPlayerClassData( desiredClassVector[ which ] ); + if ( classData ) + { + return classData->m_szClassName; + } + + Warning( "TFBot unable to get data for desired class, defaulting to 'auto'\n" ); + return "auto"; +} + + +//----------------------------------------------------------------------------------------------------- +CTFBot::CTFBot() +{ + m_body = new CTFBotBody( this ); + m_locomotor = new CTFBotLocomotion( this ); + m_vision = new CTFBotVision( this ); + ALLOCATE_INTENTION_INTERFACE( CTFBot ); + + m_spawnArea = NULL; + m_weaponRestrictionFlags = 0; + m_attributeFlags = 0; + m_homeArea = NULL; + m_squad = NULL; + m_didReselectClass = false; + m_enemySentry = NULL; + m_spotWhereEnemySentryLastInjuredMe = vec3_origin; + m_isLookingAroundForEnemies = true; + m_behaviorFlags = 0; + m_attentionFocusEntity = NULL; + m_noisyTimer.Invalidate(); + + if ( TFGameRules()->IsInTraining() ) + { + m_difficulty = CTFBot::EASY; + } + else + { + m_difficulty = clamp( (CTFBot::DifficultyType)tf_bot_difficulty.GetInt(), CTFBot::EASY, CTFBot::EXPERT ); + } + + m_actionPoint = NULL; + m_proxy = NULL; + m_spawner = NULL; + + m_myControlPoint = NULL; + + SetMission( NO_MISSION, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM ); + SetMissionTarget( NULL ); + m_missionString.Clear(); + + m_fModelScaleOverride = -1.0f; + m_maxVisionRangeOverride = -1.0f; + m_squadFormationError = 0.0f; + + m_hFollowingFlagTarget = NULL; + + SetShouldQuickBuild( false ); + SetAutoJump( 0.f, 0.f ); + + ClearSniperSpots(); + + ListenForGameEvent( "teamplay_point_startcapture" ); + ListenForGameEvent( "teamplay_point_captured" ); + ListenForGameEvent( "teamplay_round_win" ); + ListenForGameEvent( "teamplay_flag_event" ); +} + + +//----------------------------------------------------------------------------------------------------- +CTFBot::~CTFBot() +{ + // delete Intention first, since destruction of Actions may access other components + DEALLOCATE_INTENTION_INTERFACE; + + if ( m_body ) + delete m_body; + + if ( m_locomotor ) + delete m_locomotor; + + if ( m_vision ) + delete m_vision; + + m_suspectedSpyVector.PurgeAndDeleteElements(); +} + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::Spawn() +{ + BaseClass::Spawn(); + + m_spawnArea = NULL; + m_justLostPointTimer.Invalidate(); + m_squad = NULL; + m_didReselectClass = false; + m_isLookingAroundForEnemies = true; + m_attentionFocusEntity = NULL; + + m_suspectedSpyVector.PurgeAndDeleteElements(); + m_knownSpyVector.RemoveAll(); + m_delayedNoticeVector.RemoveAll(); + + m_myControlPoint = NULL; + ClearSniperSpots(); + ClearTags(); + + m_hFollowingFlagTarget = NULL; + + m_requiredWeaponStack.Clear(); + SetShouldQuickBuild( false ); + + SetSquadFormationError( 0.0f ); + SetBrokenFormation( false ); + + GetVisionInterface()->ForgetAllKnownEntities(); +} + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::SetMission( MissionType mission, bool resetBehaviorSystem ) +{ + SetPrevMission( m_mission ); + m_mission = mission; + + if ( resetBehaviorSystem ) + { + // reset the behavior system to start the given mission + GetIntentionInterface()->Reset(); + } + + // Temp hack - some missions play an idle loop + if ( m_mission > NO_MISSION ) + { + StartIdleSound(); + } +} + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::PhysicsSimulate( void ) +{ + BaseClass::PhysicsSimulate(); + + if ( m_spawnArea == NULL ) + { + m_spawnArea = GetLastKnownArea(); + } + + if ( HasAttribute( CTFBot::ALWAYS_CRIT ) && !m_Shared.InCond( TF_COND_CRITBOOSTED_USER_BUFF ) ) + { + m_Shared.AddCond( TF_COND_CRITBOOSTED_USER_BUFF ); + } + + // force my speed to be recalculated to keep squad together and restore speed afterwards + TeamFortress_SetSpeed(); + + if ( IsInASquad() ) + { + if ( GetSquad()->GetMemberCount() <= 1 || GetSquad()->GetLeader() == NULL ) + { + // squad has collapsed - disband it + LeaveSquad(); + } + } + + + // If we're dead, choose a new class. + // We need to do this outside of the behavior system, since changing class can + // sometimes force an immediate respawn, which will destroy the bot's existing actions out from under it. + if ( !IsAlive() && !m_didReselectClass && tf_bot_keep_class_after_death.GetBool() == false && TFGameRules()->CanBotChangeClass( this ) ) + { + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + return; + + const char *classname = FStrEq( tf_bot_force_class.GetString(), "" ) ? GetNextSpawnClassname() : tf_bot_force_class.GetString(); + + HandleCommand_JoinClass( classname ); + + m_didReselectClass = true; + } +} + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::Touch( CBaseEntity *pOther ) +{ + BaseClass::Touch( pOther ); + + CTFPlayer *them = ToTFPlayer( pOther ); + if ( them && IsEnemy( them ) ) + { + if ( them->m_Shared.IsStealthed() || them->m_Shared.InCond( TF_COND_DISGUISED ) ) + { + // bumped a spy - they are discovered! + if ( TFGameRules()->IsMannVsMachineMode() ) // we have to build up to knowing that they are a spy in MvM + { + SuspectSpy( them ); + } + else + { + RealizeSpy( them ); + } + } + + // always notice if we bump an enemy + TheNextBots().OnWeaponFired( them, them->GetActiveTFWeapon() ); + } +} + + +//----------------------------------------------------------------------------------------------------- +// Avoid penetrating teammates +void CTFBot::AvoidPlayers( CUserCmd *pCmd ) +{ + // Turn off the avoid player code. + if ( !tf_avoidteammates.GetBool() || !tf_avoidteammates_pushaway.GetBool() ) + return; + + Vector forward, right; + EyeVectors( &forward, &right ); + + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS ); + + Vector avoidVector = vec3_origin; + + float tooClose = 50.0f; + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + // bots stay farther apart in MvM mode + tooClose = 150.0f; + } + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *them = playerVector[i]; + + if ( IsSelf( them ) ) + { + continue; + } + + if ( HasTheFlag() ) + { + // Don't push around the flag (bomb) carrier. + // We need this for MvM mode so friendly bots don't + // move the bomb jumper and cause him to restart. + continue; + } + + if ( IsPlayerClass( TF_CLASS_MEDIC ) ) + { + if ( !them->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + // medics only avoid other medics, so they stay with their patient + continue; + } + } + else if ( IsInASquad() ) + { + // if I'm a non-Medic in a Squad, I'm part of a formation + continue; + } + + Vector between = GetAbsOrigin() - them->GetAbsOrigin(); + if ( between.IsLengthLessThan( tooClose ) ) + { + float range = between.NormalizeInPlace(); + + avoidVector += ( 1.0f - ( range / tooClose ) ) * between; + } + } + + if ( avoidVector.IsZero() ) + { + m_Shared.SetSeparation( false ); + m_Shared.SetSeparationVelocity( vec3_origin ); + return; + } + + avoidVector.NormalizeInPlace(); + + m_Shared.SetSeparation( true ); + + const float maxSpeed = 50.0f; + m_Shared.SetSeparationVelocity( avoidVector * maxSpeed ); + + float ahead = maxSpeed * DotProduct( forward, avoidVector ); + float side = maxSpeed * DotProduct( right, avoidVector ); + + pCmd->forwardmove += ahead; + pCmd->sidemove += side; +} + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::UpdateOnRemove( void ) +{ + StopIdleSound(); + + BaseClass::UpdateOnRemove(); +} + + +//----------------------------------------------------------------------------------------------------- +int CTFBot::ShouldTransmit( const CCheckTransmitInfo *pInfo ) +{ + if ( HasAttribute( USE_BOSS_HEALTH_BAR ) ) + { + return FL_EDICT_ALWAYS; + } + + return BaseClass::ShouldTransmit( pInfo ); +} + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::ChangeTeam( int iTeamNum, bool bAutoTeam, bool bSilent, bool bAutoBalance /*= false*/ ) +{ + BaseClass::ChangeTeam( iTeamNum, bAutoTeam, bSilent, bAutoBalance ); + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + SetPrevMission( CTFBot::NO_MISSION ); + ClearAllAttributes(); + // Clear Sound + StopIdleSound(); + } +} + + +//----------------------------------------------------------------------------------------------------- +bool CTFBot::ShouldGib( const CTakeDamageInfo &info ) +{ + // only gib giant/miniboss + if ( TFGameRules()->IsMannVsMachineMode() && ( IsMiniBoss() || GetModelScale() > 1.f ) ) + { + return true; + } + + return BaseClass::ShouldGib( info ); +} + + +//----------------------------------------------------------------------------------------------------- +bool CTFBot::IsAllowedToPickUpFlag( void ) const +{ + if ( !BaseClass::IsAllowedToPickUpFlag() ) + { + return false; + } + + // only the leader of a squad can pick up the flag + if ( IsInASquad() && !GetSquad()->IsLeader( const_cast< CTFBot * >( this ) ) ) + return false; + + // mission bots can't pick up the flag + return !IsOnAnyMission(); +} + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::InitClass( void ) +{ + BaseClass::InitClass(); +} + +void CTFBot::ModifyMaxHealth( int nNewMaxHealth, bool bSetCurrentHealth /*= true*/, bool bAllowModelScaling /*= true*/ ) +{ + if ( GetMaxHealth() != nNewMaxHealth ) + { + static CSchemaAttributeDefHandle pAttrDef_HiddenMaxHealthNonBuffed( "hidden maxhealth non buffed" ); + if ( !pAttrDef_HiddenMaxHealthNonBuffed ) + { + Warning( "TFBotSpawner: Invalid attribute 'hidden maxhealth non buffed'\n" ); + } + else + { + CAttributeList *pAttrList = GetAttributeList(); + if ( pAttrList ) + { + pAttrList->SetRuntimeAttributeValue( pAttrDef_HiddenMaxHealthNonBuffed, nNewMaxHealth - GetMaxHealth() ); + } + } + } + + if ( bSetCurrentHealth ) + { + SetHealth( nNewMaxHealth ); + } + + if ( bAllowModelScaling && IsMiniBoss() ) + { + SetModelScale( m_fModelScaleOverride > 0.0f ? m_fModelScaleOverride : tf_mvm_miniboss_scale.GetFloat() ); + } +} + +//----------------------------------------------------------------------------------------------------- +/** + * Invoked when a game event occurs + */ +void CTFBot::FireGameEvent( IGameEvent *event ) +{ + const char *eventName = event->GetName(); + + if ( FStrEq( eventName, "teamplay_point_captured" ) ) + { + ClearMyControlPoint(); + + int whoCapped = event->GetInt( "team" ); + int pointID = event->GetInt( "cp" ); + + if ( whoCapped == GetTeamNumber() ) + { + OnTerritoryCaptured( pointID ); + } + else + { + OnTerritoryLost( pointID ); + + m_justLostPointTimer.Start( RandomFloat( 10.0f, 20.0f ) ); + } + } + else if ( FStrEq( eventName, "teamplay_point_startcapture" ) ) + { + int pointID = event->GetInt( "cp" ); + + OnTerritoryContested( pointID ); + } + else if ( FStrEq( eventName, "teamplay_flag_event" ) ) + { + if ( event->GetInt( "eventtype" ) == TF_FLAGEVENT_PICKUP ) + { + int iPlayer = event->GetInt( "player" ); + if ( iPlayer == entindex() ) + { + // I just picked up the flag + OnPickUp( NULL, NULL ); + } + } + } +} + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::Event_Killed( const CTakeDamageInfo &info ) +{ + BaseClass::Event_Killed( info ); + + if ( HasProxy() ) + { + GetProxy()->OnKilled(); + } + + // announce Spies + if ( TFGameRules()->IsMannVsMachineMode() ) + { + if ( IsPlayerClass( TF_CLASS_SPY ) ) + { + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS ); + + int spyCount = 0; + for( int i=0; i<playerVector.Count(); ++i ) + { + if ( playerVector[i]->IsPlayerClass( TF_CLASS_SPY ) ) + { + ++spyCount; + } + } + + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_mission_update" ); + if ( event ) + { + event->SetInt( "class", TF_CLASS_SPY ); + event->SetInt( "count", spyCount ); + gameeventmanager->FireEvent( event ); + } + } + else if ( IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + // in MVM, when an engineer dies, we need to decouple his objects so they stay alive when his bot slot gets recycled + while ( GetObjectCount() > 0 ) + { + // set to not have owner + CBaseObject *pObject = GetObject( 0 ); + if ( pObject ) + { + pObject->SetOwnerEntity( NULL ); + pObject->SetBuilder( NULL ); + } + RemoveObject( pObject ); + } + + // unown engineer nest if owned any + for ( int i=0; i<ITFBotHintEntityAutoList::AutoList().Count(); ++i ) + { + CBaseTFBotHintEntity* pHint = static_cast< CBaseTFBotHintEntity* >( ITFBotHintEntityAutoList::AutoList()[i] ); + if ( pHint->GetOwnerEntity() == this ) + { + pHint->SetOwnerEntity( NULL ); + } + } + + CUtlVector< CTFPlayer* > playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS ); + bool bShouldAnnounceLastEngineerBotDeath = HasAttribute( CTFBot::TELEPORT_TO_HINT ); + if ( bShouldAnnounceLastEngineerBotDeath ) + { + for ( int i=0; i<playerVector.Count(); ++i ) + { + if ( playerVector[i] != this && playerVector[i]->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + bShouldAnnounceLastEngineerBotDeath = false; + break; + } + } + } + + if ( bShouldAnnounceLastEngineerBotDeath ) + { + bool bEngineerTeleporterInTheWorld = false; + for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i ) + { + CBaseObject* pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] ); + if ( pObj->GetType() == OBJ_TELEPORTER && pObj->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + bEngineerTeleporterInTheWorld = true; + } + } + + if ( bEngineerTeleporterInTheWorld ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_An_Engineer_Bot_Is_Dead_But_Not_Teleporter" ); + } + else + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_An_Engineer_Bot_Is_Dead" ); + } + } + } + + // remove this bot from following flag + for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i ) + { + for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i ) + { + CCaptureFlag *flag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[i] ); + flag->RemoveFollower( this ); + } + } + } // MvM + + if ( HasSpawner() ) + { + GetSpawner()->OnBotKilled( this ); + } + + if ( IsInASquad() ) + { + LeaveSquad(); + } + + CTFNavArea *lastArea = (CTFNavArea *)GetLastKnownArea(); + if ( lastArea ) + { + // remove us from old visible set + NavAreaCollector wasVisible; + lastArea->ForAllPotentiallyVisibleAreas( wasVisible ); + + int i; + for( i=0; i<wasVisible.m_area.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)wasVisible.m_area[i]; + area->RemovePotentiallyVisibleActor( this ); + } + } + + + if ( info.GetInflictor() && info.GetInflictor()->GetTeamNumber() != GetTeamNumber() ) + { + CObjectSentrygun *sentrygun = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() ); + + if ( sentrygun ) + { + // we were killed by an enemy sentry - remember it + RememberEnemySentry( sentrygun, GetAbsOrigin() ); + } + } + + StopIdleSound(); +} + + +//----------------------------------------------------------------------------------------------------- +CTeamControlPoint *CTFBot::SelectPointToCapture( CUtlVector< CTeamControlPoint * > *captureVector ) const +{ + if ( !captureVector || captureVector->Count() == 0 ) + { + return NULL; + } + + if ( captureVector->Count() == 1 ) + { + // only one choice + return captureVector->Element(0); + } + + // if we're capturing a point, stay on it + if ( const_cast< CTFBot * >( this )->IsCapturingPoint() ) + { + CTriggerAreaCapture *trigger = const_cast< CTFBot * >( this )->GetControlPointStandingOn(); + if ( trigger ) + { + return trigger->GetControlPoint(); + } + } + + // if we're near a point that is being captured, go help (in the event multiple points are being simultaneously captured) + CTeamControlPoint *closestPoint = SelectClosestControlPointByTravelDistance( captureVector ); + if ( closestPoint ) + { + bool alwaysUseClosest = false; + +#ifdef STAGING_ONLY + alwaysUseClosest = TFGameRules() && TFGameRules()->IsBountyMode(); +#endif // STAGING_ONLY + + if ( IsPointBeingCaptured( closestPoint ) || alwaysUseClosest ) + { + return closestPoint; + } + } + + // if any point is being captured by our team, go help + for( int i=0; i<captureVector->Count(); ++i ) + { + CTeamControlPoint *point = captureVector->Element(i); + + if ( IsPointBeingCaptured( point ) ) + { + return point; + } + } + + // no points are currently being captured - pick the point with the least combat + CTeamControlPoint *safestPoint = NULL; + float safestPointCombat = FLT_MAX; + bool areAllPointsCombatFree = true; + + for( int i=0; i<captureVector->Count(); ++i ) + { + CTeamControlPoint *point = captureVector->Element(i); + CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() ); + + if ( !pointArea ) + { + continue; + } + + float combat = pointArea->GetCombatIntensity(); + + const float minCombat = 0.1f; + if ( combat > minCombat ) + { + areAllPointsCombatFree = false; + } + + if ( combat < safestPointCombat ) + { + safestPoint = point; + safestPointCombat = combat; + } + } + + // if no points are in combat, pick a random point + if ( areAllPointsCombatFree ) + { + const float decisionPeriod = 60.0f; + int which = captureVector->Count() * TransientlyConsistentRandomValue( decisionPeriod ); + which = clamp( which, 0, captureVector->Count()-1 ); + + return captureVector->Element( which ); + } + + // choose the point with the least combat + return safestPoint; +} + + +//--------------------------------------------------------------------------------------------- +CTeamControlPoint *CTFBot::SelectPointToDefend( CUtlVector< CTeamControlPoint * > *defendVector ) const +{ + if ( defendVector && defendVector->Count() > 0 ) + { + if ( HasAttribute( CTFBot::PRIORITIZE_DEFENSE ) ) + { + return SelectClosestControlPointByTravelDistance( defendVector ); + } + + return defendVector->Element( RandomInt( 0, defendVector->Count()-1 ) ); + } + + return NULL; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Return the point we have decided to capture or defend + */ +CTeamControlPoint *CTFBot::GetMyControlPoint( void ) const +{ + if ( m_myControlPoint != NULL && !m_evaluateControlPointTimer.IsElapsed() ) + { + return m_myControlPoint; + } + + m_evaluateControlPointTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + + CUtlVector< CTeamControlPoint * > captureVector; + TFGameRules()->CollectCapturePoints( const_cast< CTFBot * >( this ), &captureVector ); + + CUtlVector< CTeamControlPoint * > defendVector; + TFGameRules()->CollectDefendPoints( const_cast< CTFBot * >( this ), &defendVector ); + + if ( IsPlayerClass( TF_CLASS_ENGINEER ) || IsPlayerClass( TF_CLASS_SNIPER ) || HasAttribute( CTFBot::PRIORITIZE_DEFENSE ) ) + { + // engineers always try to defend first + if ( defendVector.Count() > 0 ) + { + m_myControlPoint = SelectPointToDefend( &defendVector ); + return m_myControlPoint; + } + } + + // if we have a point we can capture - do it + m_myControlPoint = SelectPointToCapture( &captureVector ); + + if ( m_myControlPoint == NULL ) + { + // otherwise, defend our point(s) from capture + m_myControlPoint = SelectPointToDefend( &defendVector ); + } + + return m_myControlPoint; +} + + +//----------------------------------------------------------------------------------------------------- +// Return flag we want to fetch +CCaptureFlag *CTFBot::GetFlagToFetch( void ) const +{ + CUtlVector<CCaptureFlag *> flagsVector; + int nCarriedFlags = 0; + + // MvM Engineer bot never pick up a flag + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + if ( GetTeamNumber() == TF_TEAM_PVE_INVADERS && IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + return NULL; + } + + if( HasAttribute( CTFBot::IGNORE_FLAG ) ) + { + return NULL; + } + + if ( TFGameRules()->IsMannVsMachineMode() && HasFlagTaget() ) + { + return GetFlagTarget(); + } + } + + // Collect flags + for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i ) + { + CCaptureFlag *flag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[i] ); + + if ( flag->IsDisabled() ) + continue; + + // If I'm carrying a flag, look for mine and early-out + if ( HasTheFlag() ) + { + if ( flag->GetOwnerEntity() == this ) + { + return flag; + } + } + + switch( flag->GetType() ) + { + case TF_FLAGTYPE_CTF: + if ( flag->GetTeamNumber() == GetEnemyTeam( GetTeamNumber() ) ) + { + // we want to steal the other team's flag + flagsVector.AddToTail( flag ); + } + break; + + case TF_FLAGTYPE_ATTACK_DEFEND: + case TF_FLAGTYPE_TERRITORY_CONTROL: + case TF_FLAGTYPE_INVADE: + if ( flag->GetTeamNumber() != GetEnemyTeam( GetTeamNumber() ) ) + { + // we want to move our team's flag or a neutral flag + flagsVector.AddToTail( flag ); + } + break; + } + + if ( flag->IsStolen() ) + { + nCarriedFlags++; + } + } + + CCaptureFlag *pClosestFlag = NULL; + float flClosestFlagDist = FLT_MAX; + CCaptureFlag *pClosestUncarriedFlag = NULL; + float flClosestUncarriedFlagDist = FLT_MAX; + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + int nMinFollower = INT_MAX; + + FOR_EACH_VEC( flagsVector, i ) + { + CCaptureFlag *pFlag = flagsVector[i]; + if ( pFlag ) + { + // find the one which needs the most love + if ( pFlag->GetNumFollowers() < nMinFollower ) + { + nMinFollower = pFlag->GetNumFollowers(); + + pClosestFlag = NULL; + flClosestFlagDist = FLT_MAX; + pClosestUncarriedFlag = NULL; + flClosestUncarriedFlagDist = FLT_MAX; + } + + if ( pFlag->GetNumFollowers() == nMinFollower ) + { + // Find the closest + float flDist = ( pFlag->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); + if ( flDist < flClosestFlagDist ) + { + pClosestFlag = pFlag; + flClosestFlagDist = flDist; + } + + // Find the closest uncarried + if ( nCarriedFlags < flagsVector.Count() && !pFlag->IsStolen() ) + { + if ( flDist < flClosestUncarriedFlagDist ) + { + pClosestUncarriedFlag = flagsVector[i]; + flClosestUncarriedFlagDist = flDist; + } + } + } + } + } + } + else + { + FOR_EACH_VEC( flagsVector, i ) + { + if ( flagsVector[i] ) + { + // Find the closest + float flDist = ( flagsVector[i]->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); + if ( flDist < flClosestFlagDist ) + { + pClosestFlag = flagsVector[i]; + flClosestFlagDist = flDist; + } + + // Find the closest uncarried + if ( nCarriedFlags < flagsVector.Count() && !flagsVector[i]->IsStolen() ) + { + if ( flDist < flClosestUncarriedFlagDist ) + { + pClosestUncarriedFlag = flagsVector[i]; + flClosestUncarriedFlagDist = flDist; + } + } + } + } + } + + // If we have an uncarried flag, prioritize + if ( pClosestUncarriedFlag ) + return pClosestUncarriedFlag; + + return pClosestFlag; +} + + +//----------------------------------------------------------------------------------------------------- +// Return capture zone for our flag(s) +CCaptureZone *CTFBot::GetFlagCaptureZone( void ) const +{ + for( int i=0; i<ICaptureZoneAutoList::AutoList().Count(); ++i ) + { + CCaptureZone *zone = static_cast< CCaptureZone* >( ICaptureZoneAutoList::AutoList()[i] ); + if ( zone->GetTeamNumber() == GetTeamNumber() ) + { + return zone; + } + } + + return NULL; +} + + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::ClearMyControlPoint( void ) +{ + m_myControlPoint = NULL; + m_evaluateControlPointTimer.Invalidate(); +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Return true if no enemy has contested any point yet + */ +bool CTFBot::AreAllPointsUncontestedSoFar( void ) const +{ + CTeamControlPointMaster *master = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( master ) + { + for( int i=0; i<master->GetNumPoints(); ++i ) + { + CTeamControlPoint *point = master->GetControlPoint( i ); + + if ( point && point->HasBeenContested() ) + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------------------------------------- +// Return true if the given point is being captured +bool CTFBot::IsPointBeingCaptured( CTeamControlPoint *point ) const +{ + if ( point == NULL ) + return false; + + if ( point->LastContestedAt() > 0.0f && ( gpGlobals->curtime - point->LastContestedAt() ) < 5.0f ) + { + // the point is, or was very recently, contested + return true; + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +// Return true if any point is being captured +bool CTFBot::IsAnyPointBeingCaptured( void ) const +{ + CTeamControlPointMaster *master = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( master ) + { + for( int i=0; i<master->GetNumPoints(); ++i ) + { + CTeamControlPoint *point = master->GetControlPoint( i ); + + if ( IsPointBeingCaptured( point ) ) + return true; + } + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +// Return true if we are within a short travel distance of the current point +bool CTFBot::IsNearPoint( CTeamControlPoint *point ) const +{ + CTFNavArea *myArea = GetLastKnownArea(); + + if ( !myArea || !point ) + { + return false; + } + + CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() ); + + if ( !pointArea ) + { + return false; + } + + float travelToPoint = fabs( myArea->GetIncursionDistance( GetTeamNumber() ) - pointArea->GetIncursionDistance( GetTeamNumber() ) ); + + return travelToPoint < tf_bot_near_point_travel_distance.GetFloat(); +} + + +//--------------------------------------------------------------------------------------------- +// Return time left to capture the point before we lose the game +float CTFBot::GetTimeLeftToCapture( void ) const +{ + if ( TFGameRules()->IsInKothMode() ) + { + if ( TFGameRules()->GetKothTeamTimer( GetEnemyTeam( GetTeamNumber() ) ) ) + { + return TFGameRules()->GetKothTeamTimer( GetEnemyTeam( GetTeamNumber() ) )->GetTimeRemaining(); + } + } + else if ( TFGameRules()->GetActiveRoundTimer() ) + { + return TFGameRules()->GetActiveRoundTimer()->GetTimeRemaining(); + } + + return 0.0f; +} + + +//----------------------------------------------------------------------------------------------------- +// Do internal setup when control point changes +void CTFBot::SetupSniperSpotAccumulation( void ) +{ + VPROF_BUDGET( "CTFBot::SetupSniperSpotAccumulation", "NextBot" ); + + CBaseEntity *goalEntity = NULL; + + if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT ) + { + // try to find a payload cart to guard + CTeamTrainWatcher *trainWatcher = TFGameRules()->GetPayloadToPush( GetTeamNumber() ); + + if ( !trainWatcher ) + { + trainWatcher = TFGameRules()->GetPayloadToBlock( GetTeamNumber() ); + } + + if ( trainWatcher ) + { + goalEntity = trainWatcher->GetTrainEntity(); + } + } + else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP ) + { + goalEntity = GetMyControlPoint(); + } + + if ( !goalEntity ) + { + ClearSniperSpots(); + return; + } + + if ( goalEntity == m_snipingGoalEntity ) + { + // if goal has moved too much (ie: payload cart), recompute our spots + Vector toGoal = m_snipingGoalEntity->WorldSpaceCenter() - m_lastSnipingGoalEntityPosition; + + if ( toGoal.IsLengthLessThan( tf_bot_sniper_goal_entity_move_tolerance.GetFloat() ) ) + { + // already set up + return; + } + } + + ClearSniperSpots(); + + int myTeam = GetTeamNumber(); + int enemyTeam = ( myTeam == TF_TEAM_BLUE ) ? TF_TEAM_RED : TF_TEAM_BLUE; + + bool isDefendingPoint = false; + CTFNavArea *goalEntityArea = NULL; + + if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT ) + { + // the cart is owned by the invaders + isDefendingPoint = ( goalEntity->GetTeamNumber() != myTeam ); + goalEntityArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( goalEntity->WorldSpaceCenter(), GETNAVAREA_CHECK_GROUND, 500.0f ); + } + else + { + isDefendingPoint = ( GetMyControlPoint()->GetOwner() == myTeam ); + goalEntityArea = TheTFNavMesh()->GetControlPointCenterArea( GetMyControlPoint()->GetPointIndex() ); + } + + // we are sniping a different control point - setup for new point accumulation + m_sniperVantageAreaVector.RemoveAll(); + m_sniperTheaterAreaVector.RemoveAll(); + + if ( !goalEntityArea ) + { + return; + } + + for( int i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)TheNavAreas[i]; + + if ( !area->IsReachableByTeam( myTeam ) || !area->IsReachableByTeam( enemyTeam ) ) + { + continue; + } + + if ( area->GetIncursionDistance( enemyTeam ) <= goalEntityArea->GetIncursionDistance( enemyTeam ) ) + { + m_sniperTheaterAreaVector.AddToTail( area ); + } + + // if this is my point, I can stand on it, or go a bit beyond it + float myIncursionTolerance = tf_bot_sniper_spot_point_tolerance.GetFloat(); + + if ( !isDefendingPoint ) + { + // not my point, keep back from it a bit + myIncursionTolerance *= -1.0f; + } + + if ( area->GetIncursionDistance( myTeam ) <= goalEntityArea->GetIncursionDistance( myTeam ) + myIncursionTolerance ) + { + m_sniperVantageAreaVector.AddToTail( area ); + } + } + + m_snipingGoalEntity = goalEntity; + m_lastSnipingGoalEntityPosition = goalEntity->WorldSpaceCenter(); +} + + +//----------------------------------------------------------------------------------------------------- +// Randomly sample points within candidate areas to find good sniping positions +void CTFBot::AccumulateSniperSpots( void ) +{ + VPROF_BUDGET( "CTFBot::AccumulateSniperSpots", "NextBot" ); + + SetupSniperSpotAccumulation(); + + if ( m_sniperVantageAreaVector.Count() == 0 || m_sniperTheaterAreaVector.Count() == 0 ) + { + // retry every so often to catch cases where the incursion data is invalid during setup time + // due to blocked/closed off areas, etc. + if ( m_retrySniperSpotSetupTimer.IsElapsed() ) + { + // retry + ClearSniperSpots(); + } + + return; + } + + SniperSpotInfo info; + + for( int count=0; count<tf_bot_sniper_spot_search_count.GetInt(); ++count ) + { + // pick a random vantage area to sample + int which = RandomInt( 0, m_sniperVantageAreaVector.Count()-1 ); + info.m_vantageArea = m_sniperVantageAreaVector[ which ]; + info.m_vantageSpot = info.m_vantageArea->GetRandomPoint(); + + // pick a random theater area to sample + which = RandomInt( 0, m_sniperTheaterAreaVector.Count()-1 ); + info.m_theaterArea = m_sniperTheaterAreaVector[ which ]; + info.m_theaterSpot = info.m_theaterArea->GetRandomPoint(); + + info.m_range = ( info.m_vantageSpot - info.m_theaterSpot ).Length(); + if ( info.m_range < tf_bot_sniper_spot_min_range.GetFloat() ) + { + // not long enough sightline + continue; + } + + for( int i=0; i<m_sniperSpotVector.Count(); ++i ) + { + if ( ( info.m_vantageSpot - m_sniperSpotVector[i].m_vantageSpot ).IsLengthLessThan( tf_bot_sniper_spot_epsilon.GetFloat() ) ) + { + // too close to existing spot + continue; + } + } + + Vector eyeOffset( 0, 0, 60.0f ); + if ( IsLineOfFireClear( info.m_vantageSpot + eyeOffset, info.m_theaterSpot + eyeOffset ) ) + { + // valid spot + + // maximize the time it takes the enemy to get to us + info.m_advantage = info.m_vantageArea->GetIncursionDistance( GetEnemyTeam( GetTeamNumber() ) ) - info.m_theaterArea->GetIncursionDistance( GetEnemyTeam( GetTeamNumber() ) ); + + // if we have already maxxed out our sniper spots, replace the worst one if this is better + if ( m_sniperSpotVector.Count() >= tf_bot_sniper_spot_max_count.GetInt() ) + { + int worst = -1; + + for( int i=0; i<m_sniperSpotVector.Count(); ++i ) + { + if ( worst < 0 || m_sniperSpotVector[i].m_advantage < m_sniperSpotVector[ worst ].m_advantage ) + { + worst = i; + } + } + + // if our new spot is better, replace it + if ( info.m_advantage > m_sniperSpotVector[ worst ].m_advantage ) + { + m_sniperSpotVector[ worst ] = info; + } + } + else + { + m_sniperSpotVector.AddToTail( info ); + } + } + } + + if ( IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + for( int i=0; i<m_sniperSpotVector.Count(); ++i ) + { + NDebugOverlay::Cross3D( m_sniperSpotVector[i].m_vantageSpot, 5.0f, 255, 0, 255, true, 0.1f ); + NDebugOverlay::Line( m_sniperSpotVector[i].m_vantageSpot, m_sniperSpotVector[i].m_theaterSpot, 0, 200, 0, true, 0.1f ); + } + } +} + + + +//----------------------------------------------------------------------------------------------------- +void CTFBot::ClearSniperSpots( void ) +{ + m_sniperSpotVector.RemoveAll(); + m_sniperVantageAreaVector.RemoveAll(); + m_sniperTheaterAreaVector.RemoveAll(); + m_snipingGoalEntity = NULL; + m_retrySniperSpotSetupTimer.Start( RandomFloat( 5.0f, 10.0f ) ); +} + + + +//--------------------------------------------------------------------------------------------- +class CCollectReachableObjects : public ISearchSurroundingAreasFunctor +{ +public: + CCollectReachableObjects( const CTFBot *me, float maxRange, const CUtlVector< CHandle< CBaseEntity > > &potentialVector, CUtlVector< CHandle< CBaseEntity > > *collectionVector ) : m_potentialVector( potentialVector ) + { + m_me = me; + m_maxRange = maxRange; + m_collectionVector = collectionVector; + } + + virtual bool operator() ( CNavArea *area, CNavArea *priorArea, float travelDistanceSoFar ) + { + // do any of the potential objects overlap this area? + FOR_EACH_VEC( m_potentialVector, it ) + { + CBaseEntity *obj = m_potentialVector[ it ]; + + if ( obj && area->Contains( obj->WorldSpaceCenter() ) ) + { + // reachable - keep it + if ( !m_collectionVector->HasElement( obj ) ) + { + m_collectionVector->AddToTail( obj ); + } + } + } + return true; + } + + virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar ) + { + if ( adjArea->IsBlocked( m_me->GetTeamNumber() ) ) + { + return false; + } + + if ( travelDistanceSoFar > m_maxRange ) + { + // too far away + return false; + } + + return currentArea->IsContiguous( adjArea ); + } + + const CTFBot *m_me; + float m_maxRange; + const CUtlVector< CHandle< CBaseEntity > > &m_potentialVector; + CUtlVector< CHandle< CBaseEntity > > *m_collectionVector; +}; + + +// +// Search outwards from startSearchArea and collect all reachable objects from the given list that pass the given filter +// Items in selectedObjectVector will be approximately sorted in nearest-to-farthest order (because of SearchSurroundingAreas) +// +void CTFBot::SelectReachableObjects( const CUtlVector< CHandle< CBaseEntity > > &candidateObjectVector, + CUtlVector< CHandle< CBaseEntity > > *selectedObjectVector, + const INextBotFilter &filter, + CNavArea *startSearchArea, + float maxRange ) const +{ + if ( startSearchArea == NULL || selectedObjectVector == NULL ) + return; + + selectedObjectVector->RemoveAll(); + + // filter candidate objects + CUtlVector< CHandle< CBaseEntity > > filteredObjectVector; + for( int i=0; i<candidateObjectVector.Count(); ++i ) + { + if ( filter.IsSelected( candidateObjectVector[i] ) ) + { + filteredObjectVector.AddToTail( candidateObjectVector[i] ); + } + } + + // only keep those that are reachable by us + CCollectReachableObjects collector( this, maxRange, filteredObjectVector, selectedObjectVector ); + SearchSurroundingAreas( startSearchArea, collector ); +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBot::IsAmmoLow( void ) const +{ + CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon(); + if ( myWeapon ) + { + if ( myWeapon->GetWeaponID() == TF_WEAPON_WRENCH ) + { + // wrench is special. it's a melee weapon that wants ammo - metal + return ( GetAmmoCount( TF_AMMO_METAL ) <= 0 ); + } + + if ( myWeapon->IsMeleeWeapon() ) + { + // we never run out of ammo with a melee weapon + return false; + } + + // no projectile, no ammo needed + const char *weaponAlias = WeaponIdToAlias( myWeapon->GetWeaponID() ); + if ( weaponAlias ) + { + WEAPON_FILE_INFO_HANDLE weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias ); + if ( weaponInfoHandle != GetInvalidWeaponInfoHandle() ) + { + CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) ); + if ( weaponInfo && weaponInfo->GetWeaponData( TF_WEAPON_PRIMARY_MODE ).m_iProjectile == TF_PROJECTILE_NONE ) + { + // we don't shoot anything, so we don't need ammo + return false; + } + } + } + + float ratio = (float)GetAmmoCount( TF_AMMO_PRIMARY ) / (float)( const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_PRIMARY ) ); + + if ( ratio < 0.2f ) + { + return true; + } + //if ( !myWeapon->HasPrimaryAmmo() && myWeapon->GetWeaponID() != TF_WEAPON_BUILDER && myWeapon->GetWeaponID() != TF_WEAPON_MEDIGUN ) + } + + return false; +} + + +//----------------------------------------------------------------------------------------------------- +bool CTFBot::IsAmmoFull( void ) const +{ + bool isPrimaryFull = GetAmmoCount( TF_AMMO_PRIMARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_PRIMARY ); + bool isSecondaryFull = GetAmmoCount( TF_AMMO_SECONDARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_SECONDARY ); + + if ( IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + // wrench is special. it's a melee weapon that wants ammo - metal + return ( GetAmmoCount( TF_AMMO_METAL ) >= 200 ) && isPrimaryFull && isSecondaryFull; + } + + return isPrimaryFull && isSecondaryFull; + +/* + CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon(); + if ( myWeapon ) + { + if ( IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + // wrench is special. it's a melee weapon that wants ammo - metal + return ( GetAmmoCount( TF_AMMO_METAL ) >= 200 ); + } + + if ( myWeapon->IsMeleeWeapon() ) + { + // we never run out of ammo with a melee weapon + return true; + } + + // no projectile, no ammo needed + const char *weaponAlias = WeaponIdToAlias( myWeapon->GetWeaponID() ); + if ( weaponAlias ) + { + WEAPON_FILE_INFO_HANDLE weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias ); + if ( weaponInfoHandle != GetInvalidWeaponInfoHandle() ) + { + CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) ); + if ( weaponInfo && weaponInfo->GetWeaponData( TF_WEAPON_PRIMARY_MODE ).m_iProjectile == TF_PROJECTILE_NONE ) + { + // we don't shoot anything, so we don't need ammo + return true; + } + } + } + + bool isPrimaryFull = GetAmmoCount( TF_AMMO_PRIMARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_PRIMARY ); + bool isSecondaryFull = GetAmmoCount( TF_AMMO_SECONDARY ) >= const_cast< CTFBot * >( this )->GetMaxAmmo( TF_AMMO_SECONDARY ); + + return isPrimaryFull && isSecondaryFull; + } + + return false; +*/ +} + + +bool CTFBot::IsDormantWhenDead( void ) const +{ + return false; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * When someone fires their weapon + */ +void CTFBot::OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ) +{ + VPROF_BUDGET( "CTFBot::OnWeaponFired", "NextBot" ); + + BaseClass::OnWeaponFired( whoFired, weapon ); + + if ( !whoFired || !whoFired->IsAlive() ) + return; + + if ( IsRangeGreaterThan( whoFired, tf_bot_notice_gunfire_range.GetFloat() ) ) + return; + + int noticeChance = 100; + + if ( IsQuietWeapon( (CTFWeaponBase *)weapon ) ) + { + if ( IsRangeGreaterThan( whoFired, tf_bot_notice_quiet_gunfire_range.GetFloat() ) ) + { + // too far away to hear in any event + return; + } + + switch( GetDifficulty() ) + { + case EASY: + noticeChance = 10; + break; + + case NORMAL: + noticeChance = 30; + break; + + case HARD: + noticeChance = 60; + break; + + default: + case EXPERT: + noticeChance = 90; + break; + } + + if ( IsEnvironmentNoisy() ) + { + // less likely to notice with all the noise + noticeChance /= 2; + } + } + else if ( IsRangeLessThan( whoFired, 1000.0f ) ) + { + // loud gunfire in our area - it's now "noisy" for a bit + m_noisyTimer.Start( 3.0f ); + } + + if ( RandomInt( 1, 100 ) > noticeChance ) + { + return; + } + + // notice the gunfire + GetVisionInterface()->AddKnownEntity( whoFired ); +} + + +//----------------------------------------------------------------------------------------------------- +// Return true if we match the given debug symbol +bool CTFBot::IsDebugFilterMatch( const char *name ) const +{ + // player classname + if ( !Q_strnicmp( name, const_cast< CTFBot * >( this )->GetPlayerClass()->GetName(), Q_strlen( name ) ) ) + { + return true; + } + + return BaseClass::IsDebugFilterMatch( name ); +} + + +//----------------------------------------------------------------------------------------------------- +class CFindClosestPotentiallyVisibleAreaToPos +{ +public: + CFindClosestPotentiallyVisibleAreaToPos( const Vector &pos ) + { + m_pos = pos; + m_closeArea = NULL; + m_closeRangeSq = FLT_MAX; + } + + bool operator() ( CNavArea *baseArea ) + { + CTFNavArea *area = (CTFNavArea *)baseArea; + + Vector close; + area->GetClosestPointOnArea( m_pos, &close ); + + float rangeSq = ( close - m_pos ).LengthSqr(); + if ( rangeSq < m_closeRangeSq ) + { + m_closeArea = area; + m_closeRangeSq = rangeSq; + } + + return true; + } + + Vector m_pos; + CTFNavArea *m_closeArea; + float m_closeRangeSq; +}; + + +//----------------------------------------------------------------------------------------------------- +// Update our view to watch where members of the given team will be coming from +void CTFBot::UpdateLookingAroundForIncomingPlayers( bool lookForEnemies ) +{ + if ( !m_lookAtEnemyInvasionAreasTimer.IsElapsed() ) + return; + + const float maxLookInterval = 1.0f; + m_lookAtEnemyInvasionAreasTimer.Start( RandomFloat( 0.333f, maxLookInterval ) ); + + float minGazeRange = m_Shared.InCond( TF_COND_ZOOMED ) ? 750.0f : 150.0f; + + CTFNavArea *myArea = GetLastKnownArea(); + if ( myArea ) + { + int team = GetTeamNumber(); + + // if we want to look where teammates come from, we need to pass in + // the *enemy* team, since the method collects *enemy* invasion areas + if ( !lookForEnemies ) + { + team = GetEnemyTeam( team ); + } + + const CUtlVector< CTFNavArea * > &invasionAreaVector = myArea->GetEnemyInvasionAreaVector( team ); + + if ( invasionAreaVector.Count() > 0 ) + { + // try to not look directly at walls + const int retryCount = 20.0f; + for( int r=0; r<retryCount; ++r ) + { + int which = RandomInt( 0, invasionAreaVector.Count()-1 ); + Vector gazeSpot = invasionAreaVector[ which ]->GetRandomPoint() + Vector( 0, 0, 0.75f * HumanHeight ); + + if ( IsRangeGreaterThan( gazeSpot, minGazeRange ) && GetVisionInterface()->IsLineOfSightClear( gazeSpot ) ) + { + // use maxLookInterval so these looks override body aiming from path following + GetBodyInterface()->AimHeadTowards( gazeSpot, IBody::INTERESTING, maxLookInterval, NULL, "Looking toward enemy invasion areas" ); + break; + } + } + } + } +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Update our view to keep an eye on areas where the enemy will be coming from + */ +void CTFBot::UpdateLookingAroundForEnemies( void ) +{ + if ( !m_isLookingAroundForEnemies ) + return; + + if ( HasAttribute( CTFBot::IGNORE_ENEMIES ) ) + return; + + if ( m_Shared.IsControlStunned() ) + return; + + const float maxLookInterval = 1.0f; + + const CKnownEntity *known = GetVisionInterface()->GetPrimaryKnownThreat(); + + if ( known ) + { + if ( known->IsVisibleInFOVNow() ) + { + if ( IsPlayerClass( TF_CLASS_SPY ) && + GetDifficulty() >= CTFBot::HARD && + m_Shared.InCond( TF_COND_DISGUISED ) && + !m_Shared.IsStealthed() ) + { + // smart Spies don't look at their victims until it's too late... + // look around at where *teammates* will be coming from to fool the enemy + UpdateLookingAroundForIncomingPlayers( LOOK_FOR_FRIENDS ); + return; + } + + // I see you! + GetBodyInterface()->AimHeadTowards( known->GetEntity(), IBody::CRITICAL, 1.0f, NULL, "Aiming at a visible threat" ); + return; + } + +/* apparently sounds update last known position... + if ( known->WasEverVisible() && known->GetTimeSinceLastSeen() < 3.0f ) + { + // I saw you just a moment ago... + GetBodyInterface()->AimHeadTowards( known->GetLastKnownPosition() + GetClassEyeHeight(), IBody::IMPORTANT, 1.0f, NULL, "Aiming at a last known threat position" ); + return; + } +*/ + + // known but not currently visible (I know you're around here somewhere) + + // if there is unobstructed space between us, turn around + if ( IsLineOfSightClear( known->GetEntity(), IGNORE_ACTORS ) ) + { + Vector toThreat = known->GetEntity()->GetAbsOrigin() - GetAbsOrigin(); + float threatRange = toThreat.NormalizeInPlace(); + + float aimError = M_PI/6.0f; + + float s, c; + FastSinCos( aimError, &s, &c ); + + float error = threatRange * s; + Vector imperfectAimSpot = known->GetEntity()->WorldSpaceCenter(); + imperfectAimSpot.x += RandomFloat( -error, error ); + imperfectAimSpot.y += RandomFloat( -error, error ); + + GetBodyInterface()->AimHeadTowards( imperfectAimSpot, IBody::IMPORTANT, 1.0f, NULL, "Turning around to find threat out of our FOV" ); + return; + } + + if ( !IsPlayerClass( TF_CLASS_SNIPER ) ) + { + // look toward potentially visible area nearest the last known position + CTFNavArea *myArea = GetLastKnownArea(); + if ( myArea ) + { + const CTFNavArea *closeArea = NULL; + CFindClosestPotentiallyVisibleAreaToPos find( known->GetLastKnownPosition() ); + myArea->ForAllPotentiallyVisibleAreas( find ); + + closeArea = find.m_closeArea; + + if ( closeArea ) + { + // try to not look directly at walls + const int retryCount = 10.0f; + for( int r=0; r<retryCount; ++r ) + { + Vector gazeSpot = closeArea->GetRandomPoint() + Vector( 0, 0, 0.75f * HumanHeight ); + + if ( GetVisionInterface()->IsLineOfSightClear( gazeSpot ) ) + { + // use maxLookInterval so these looks override body aiming from path following + GetBodyInterface()->AimHeadTowards( gazeSpot, IBody::IMPORTANT, maxLookInterval, NULL, "Looking toward potentially visible area near known but hidden threat" ); + return; + } + } + + // can't find a clear line to look along + if ( IsDebugging( NEXTBOT_VISION | NEXTBOT_ERRORS ) ) + { + ConColorMsg( Color( 255, 255, 0, 255 ), "%3.2f: %s can't find clear line to look at potentially visible near known but hidden entity %s(#%d)\n", + gpGlobals->curtime, + GetDebugIdentifier(), + known->GetEntity()->GetClassname(), + known->GetEntity()->entindex() ); + } + } + else if ( IsDebugging( NEXTBOT_VISION | NEXTBOT_ERRORS ) ) + { + ConColorMsg( Color( 255, 255, 0, 255 ), "%3.2f: %s no potentially visible area to look toward known but hidden entity %s(#%d)\n", + gpGlobals->curtime, + GetDebugIdentifier(), + known->GetEntity()->GetClassname(), + known->GetEntity()->entindex() ); + } + } + + return; + } + } + + // no known threat - look toward where enemies will come from + UpdateLookingAroundForIncomingPlayers( LOOK_FOR_ENEMIES ); +} + + +//--------------------------------------------------------------------------------------------- +class CFindVantagePoint : public ISearchSurroundingAreasFunctor +{ +public: + CFindVantagePoint( int enemyTeamIndex ) + { + m_enemyTeamIndex = enemyTeamIndex; + m_vantageArea = NULL; + } + + virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) + { + CTFNavArea *area = (CTFNavArea *)baseArea; + + CTeam *enemyTeam = GetGlobalTeam( m_enemyTeamIndex ); + for( int i=0; i<enemyTeam->GetNumPlayers(); ++i ) + { + CTFPlayer *enemy = (CTFPlayer *)enemyTeam->GetPlayer(i); + + if ( !enemy->IsAlive() || !enemy->GetLastKnownArea() ) + continue; + + CTFNavArea *enemyArea = (CTFNavArea *)enemy->GetLastKnownArea(); + if ( enemyArea->IsCompletelyVisible( area ) ) + { + // nearby area from which we can see the enemy team + m_vantageArea = area; + return false; + } + } + + return true; + } + + int m_enemyTeamIndex; + CTFNavArea *m_vantageArea; +}; + + +//----------------------------------------------------------------------------------------------------- +// Return a nearby area where we can see a member of the enemy team +CTFNavArea *CTFBot::FindVantagePoint( float maxTravelDistance ) const +{ + CFindVantagePoint find( GetTeamNumber() == TF_TEAM_BLUE ? TF_TEAM_RED : TF_TEAM_BLUE ); + SearchSurroundingAreas( GetLastKnownArea(), find, maxTravelDistance ); + return find.m_vantageArea; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Return perceived danger of threat (0=none, 1=immediate deadly danger) + * @todo: Move this to contextual query + * @todo: Differentiate between potential threats (that sentry up ahead along our route) and immediate threats (the sentry I'm in range of) + */ +float CTFBot::GetThreatDanger( CBaseCombatCharacter *who ) const +{ + if ( who == NULL ) + return 0.0f; + + if ( IsPlayerClass( TF_CLASS_SNIPER ) ) + { + if ( IsRangeGreaterThan( who, tf_bot_sniper_personal_space_range.GetFloat() ) ) + { + // far away enemies are no threat to a Sniper + return 0.0f; + } + } + + if ( who->IsPlayer() ) + { + CTFPlayer *player = ToTFPlayer( who ); + + // ubers are scary + if ( player->m_Shared.IsInvulnerable() ) + return 1.0f; + + switch( player->GetPlayerClass()->GetClassIndex() ) + { + case TF_CLASS_MEDIC: + return 0.2f; // 1/5 + + case TF_CLASS_ENGINEER: + case TF_CLASS_SNIPER: + return 0.4f; // 2/5 + + case TF_CLASS_SCOUT: + case TF_CLASS_SPY: + case TF_CLASS_DEMOMAN: + return 0.6f; // 3/5 + + case TF_CLASS_SOLDIER: + case TF_CLASS_HEAVYWEAPONS: + return 0.8f; // 4/5 + + case TF_CLASS_PYRO: + return 1.0f; // 5/5 + } + + } + else + { + // sentry gun + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( who ); + if ( sentry ) + { + if ( !sentry->IsAlive() || sentry->IsPlacing() || sentry->HasSapper() || sentry->IsPlasmaDisabled() || sentry->IsUpgrading() || sentry->IsBuilding() ) + return 0.0f; + + switch( sentry->GetUpgradeLevel() ) + { + case 3: return 1.0f; + case 2: return 0.8f; + default: return 0.6f; + } + } + } + + return 0.0f; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Return the max range at which we can effectively attack + */ +float CTFBot::GetMaxAttackRange( void ) const +{ + CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon(); + if ( !myWeapon ) + return 0.0f; + + if ( myWeapon->IsMeleeWeapon() ) + { + return 100.0f; + } + + if ( myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) ) + { + if ( TFGameRules()->IsMannVsMachineMode() ) + { + const float flameRange = 350.0f; + + static CSchemaItemDefHandle pItemDef_GiantFlamethrower( "MVM Giant Flamethrower" ); + + if ( IsActiveTFWeapon( pItemDef_GiantFlamethrower ) ) + { + return 2.5f * flameRange; + } + + return flameRange; + } + + return 250.0f; + } + + if ( WeaponID_IsSniperRifle( myWeapon->GetWeaponID() ) ) + { + // infinite + return FLT_MAX; + } + + if ( myWeapon->IsWeapon( TF_WEAPON_ROCKETLAUNCHER ) ) + { + return 3000.0f; + } + + // bullet spray weapons, grenades, etc + // for now, default to infinite so bot always returns fire and doesn't look dumb + return FLT_MAX; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Return the ideal range at which we can effectively attack + */ +float CTFBot::GetDesiredAttackRange( void ) const +{ + CTFWeaponBase *myWeapon = m_Shared.GetActiveTFWeapon(); + if ( !myWeapon ) + return 0.0f; + + if ( myWeapon->IsWeapon( TF_WEAPON_KNIFE ) ) + { + // get very close and stab + return 70.0f; // 60 + } + + if ( myWeapon->IsMeleeWeapon() ) + { + return 100.0f; + } + + if ( myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) ) + { + return 100.0f; + } + + if ( WeaponID_IsSniperRifle( myWeapon->GetWeaponID() ) ) + { + // infinite + return FLT_MAX; + } + + if ( myWeapon->IsWeapon( TF_WEAPON_ROCKETLAUNCHER ) && !TFGameRules()->IsMannVsMachineMode() ) + { + return 1250.0f; + } + + // bullet spray weapons, grenades, etc + return 500.0f; +} + + +//----------------------------------------------------------------------------------------------------- +// If we're required to equip a specific weapon, do it. +bool CTFBot::EquipRequiredWeapon( void ) +{ + // if we have a required weapon on our stack, it takes precedence (items, etc) + if ( m_requiredWeaponStack.Count() ) + { + CBaseCombatWeapon *pWeapon = m_requiredWeaponStack.Top().Get(); + return Weapon_Switch( pWeapon ); + } + + if ( TheTFBots().IsMeleeOnly() || TFGameRules()->IsInMedievalMode() || HasWeaponRestriction( MELEE_ONLY ) ) + { + // force use of melee weapons + Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_MELEE ) ); + return true; + } + + if ( HasWeaponRestriction( PRIMARY_ONLY ) ) + { + Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ) ); + return true; + } + + if ( HasWeaponRestriction( SECONDARY_ONLY ) ) + { + Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); + return true; + } + + return false; +} + + +//----------------------------------------------------------------------------------------------------- +// Equip the best weapon we have to attack the given threat +void CTFBot::EquipBestWeaponForThreat( const CKnownEntity *threat ) +{ + if ( EquipRequiredWeapon() ) + return; + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() ) + { + if ( HasAttribute( CTFBot::AGGRESSIVE ) ) + { + // mobs never equip other weapons + return; + } + + if ( GetPlayerClass()->GetClassIndex() == TF_CLASS_DEMOMAN && !IsInASquad() ) + { + // wandering demomen use stickies only + Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); + return; + } + } +#endif // TF_RAID_MODE + + CTFWeaponBase *primary = dynamic_cast< CTFWeaponBase *>( Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ) ); + if ( !IsCombatWeapon( primary ) ) + { + primary = NULL; + } + + CTFWeaponBase *secondary = dynamic_cast< CTFWeaponBase *>( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); + if ( !IsCombatWeapon( secondary ) ) + { + secondary = NULL; + } + + // no secondary weapons in MvM + if ( TFGameRules()->IsMannVsMachineMode() ) + { + if ( IsPlayerClass( TF_CLASS_MEDIC ) && IsInASquad() && GetSquad() && !GetSquad()->IsLeader( this ) ) + { + // always try to heal leader + Weapon_Switch( Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); + return; + } + + secondary = NULL; + } + + CTFWeaponBase *melee = dynamic_cast< CTFWeaponBase *>( Weapon_GetSlot( TF_WPN_TYPE_MELEE ) ); + if ( !IsCombatWeapon( melee ) ) + { + melee = NULL; + } + + CTFWeaponBase *gun = NULL; + if ( primary ) + { + gun = primary; + } + else if ( secondary ) + { + gun = secondary; + } + else + { + gun = melee; + } + + if ( IsDifficulty( CTFBot::EASY ) ) + { + // easy bots always use their primary weapon if they have one + if ( gun ) + { + Weapon_Switch( gun ); + } + + return; + } + + if ( !threat || !threat->WasEverVisible() || threat->GetTimeSinceLastSeen() > 5.0f ) + { + // no threat - go back to primary weapon so it has a chance to reload + if ( gun ) + { + Weapon_Switch( gun ); + } + + return; + } + + // now filter weapons by available ammo + if ( GetAmmoCount( TF_AMMO_PRIMARY ) <= 0 ) + { + primary = NULL; + } + + if ( GetAmmoCount( TF_WPN_TYPE_SECONDARY ) <= 0 ) + { + secondary = NULL; + } + + // modify our gun choice based on threat situation (range, etc) + switch( GetPlayerClass()->GetClassIndex() ) + { + case TF_CLASS_DEMOMAN: + case TF_CLASS_HEAVYWEAPONS: + case TF_CLASS_SPY: + case TF_CLASS_MEDIC: + case TF_CLASS_ENGINEER: + // primary + break; + + case TF_CLASS_SCOUT: + { + if ( secondary ) + { + if ( gun && !gun->Clip1() ) + { + gun = secondary; + } + } + } + break; + + case TF_CLASS_SOLDIER: + { + // if we've emptied our rocket launcher clip and are fighting a nearby threat, switch to our secondary if it is ready to fire + if ( gun && !gun->Clip1() ) + { + if ( secondary && secondary->Clip1() ) + { + const float closeSoldierRange = 500.0f; + if ( IsRangeLessThan( threat->GetLastKnownPosition(), closeSoldierRange ) ) + { + gun = secondary; + } + } + } + } + break; + + case TF_CLASS_SNIPER: + { + const float closeSniperRange = 750.0f; + if ( secondary && IsRangeLessThan( threat->GetLastKnownPosition(), closeSniperRange ) ) + gun = secondary; + } + break; + + case TF_CLASS_PYRO: + { + const float flameRange = 750.0f; + if ( secondary && IsRangeGreaterThan( threat->GetLastKnownPosition(), flameRange ) ) + { + gun = secondary; + } + + // keep flamethrower out to reflect projectiles + if ( threat->GetEntity() && threat->GetEntity()->IsPlayer() ) + { + CTFPlayer *enemy = ToTFPlayer( threat->GetEntity() ); + + if ( enemy->IsPlayerClass( TF_CLASS_SOLDIER ) || enemy->IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + gun = primary; + } + } + } + break; + } + + if ( gun ) + { + Weapon_Switch( gun ); + } +} + + +//----------------------------------------------------------------------------------------------------- +// NOTE: This assumes default weapon loadouts +bool CTFBot::EquipLongRangeWeapon( void ) +{ + // no secondary weapons in MvM + if ( TFGameRules()->IsMannVsMachineMode() ) + return false; + + if ( IsPlayerClass( TF_CLASS_SOLDIER ) || + IsPlayerClass( TF_CLASS_DEMOMAN ) || + IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) || + IsPlayerClass( TF_CLASS_SNIPER ) ) + { + CBaseCombatWeapon *primary = Weapon_GetSlot( TF_WPN_TYPE_PRIMARY ); + if ( primary ) + { + if ( GetAmmoCount( TF_AMMO_PRIMARY ) > 0 ) + { + Weapon_Switch( primary ); + return true; + } + } + } + + // fall back to our secondary (or go right to it if its the only thing we have that has reach) + CBaseCombatWeapon *secondary = Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ); + if ( secondary ) + { + if ( GetAmmoCount( TF_AMMO_SECONDARY ) > 0 ) + { + Weapon_Switch( secondary ); + return true; + } + } + + return false; +} + + +//----------------------------------------------------------------------------------------------------- +// Force us to equip and use this weapon until popped off the required stack +void CTFBot::PushRequiredWeapon( CTFWeaponBase *weapon ) +{ + m_requiredWeaponStack.Push( weapon ); +} + + +//----------------------------------------------------------------------------------------------------- +// Pop top required weapon off of stack and discard +void CTFBot::PopRequiredWeapon( void ) +{ + m_requiredWeaponStack.Pop(); +} + + +//----------------------------------------------------------------------------------------------------- +// return true if given weapon can be used to attack +bool CTFBot::IsCombatWeapon( CTFWeaponBase *weapon ) const +{ + if ( weapon == MY_CURRENT_GUN ) // MY_CURRENT_GUN == NULL + { + weapon = m_Shared.GetActiveTFWeapon(); + } + + if ( weapon ) + { + switch ( weapon->GetWeaponID() ) + { + case TF_WEAPON_MEDIGUN: + case TF_WEAPON_PDA: + case TF_WEAPON_PDA_ENGINEER_BUILD: + case TF_WEAPON_PDA_ENGINEER_DESTROY: + case TF_WEAPON_PDA_SPY: + case TF_WEAPON_BUILDER: + case TF_WEAPON_DISPENSER: + case TF_WEAPON_INVIS: + case TF_WEAPON_LUNCHBOX: + case TF_WEAPON_BUFF_ITEM: + case TF_WEAPON_PUMPKIN_BOMB: + return false; + }; + } + + return true; +} + + +//----------------------------------------------------------------------------------------------------- +// return true if given weapon is a "hitscan" weapon +bool CTFBot::IsHitScanWeapon( CTFWeaponBase *weapon ) const +{ + if ( weapon == MY_CURRENT_GUN ) // MY_CURRENT_GUN == NULL + { + weapon = m_Shared.GetActiveTFWeapon(); + } + + if ( weapon ) + { + switch ( weapon->GetWeaponID() ) + { + case TF_WEAPON_SHOTGUN_PRIMARY: + case TF_WEAPON_SHOTGUN_SOLDIER: + case TF_WEAPON_SHOTGUN_HWG: + case TF_WEAPON_SHOTGUN_PYRO: + case TF_WEAPON_SCATTERGUN: + case TF_WEAPON_SNIPERRIFLE: + case TF_WEAPON_MINIGUN: + case TF_WEAPON_SMG: + case TF_WEAPON_CHARGED_SMG: + case TF_WEAPON_PISTOL: + case TF_WEAPON_PISTOL_SCOUT: + case TF_WEAPON_REVOLVER: + case TF_WEAPON_SENTRY_BULLET: + case TF_WEAPON_SENTRY_ROCKET: + case TF_WEAPON_SENTRY_REVENGE: + case TF_WEAPON_HANDGUN_SCOUT_PRIMARY: + case TF_WEAPON_HANDGUN_SCOUT_SECONDARY: + case TF_WEAPON_SODA_POPPER: + case TF_WEAPON_SNIPERRIFLE_DECAP: + case TF_WEAPON_PEP_BRAWLER_BLASTER: + case TF_WEAPON_SNIPERRIFLE_CLASSIC: + return true; + }; + } + + return false; +} + + +//----------------------------------------------------------------------------------------------------- +// return true if given weapon "sprays" bullets/fire/etc continuously (ie: not individual rockets/etc) +bool CTFBot::IsContinuousFireWeapon( CTFWeaponBase *weapon ) const +{ + if ( weapon == MY_CURRENT_GUN ) + { + weapon = m_Shared.GetActiveTFWeapon(); + } + + if ( !IsCombatWeapon( weapon ) ) + return false; + + if ( weapon ) + { + switch ( weapon->GetWeaponID() ) + { + case TF_WEAPON_ROCKETLAUNCHER: + case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT: + case TF_WEAPON_GRENADELAUNCHER: + case TF_WEAPON_PIPEBOMBLAUNCHER: + case TF_WEAPON_PISTOL: + case TF_WEAPON_PISTOL_SCOUT: + case TF_WEAPON_FLAREGUN: + case TF_WEAPON_JAR: + case TF_WEAPON_COMPOUND_BOW: + return false; + }; + } + + return true; + +} + + +//----------------------------------------------------------------------------------------------------- +// return true if given weapon launches explosive projectiles with splash damage +bool CTFBot::IsExplosiveProjectileWeapon( CTFWeaponBase *weapon ) const +{ + if ( weapon == MY_CURRENT_GUN ) + { + weapon = m_Shared.GetActiveTFWeapon(); + } + + if ( weapon ) + { + switch ( weapon->GetWeaponID() ) + { + case TF_WEAPON_ROCKETLAUNCHER: + case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT: + case TF_WEAPON_GRENADELAUNCHER: + case TF_WEAPON_PIPEBOMBLAUNCHER: + case TF_WEAPON_JAR: + return true; + }; + } + + return false; +} + + +//----------------------------------------------------------------------------------------------------- +// return true if given weapon has small clip and long reload cost (ie: rocket launcher, etc) +bool CTFBot::IsBarrageAndReloadWeapon( CTFWeaponBase *weapon ) const +{ + if ( weapon == MY_CURRENT_GUN ) + { + weapon = m_Shared.GetActiveTFWeapon(); + } + + if ( weapon ) + { + switch ( weapon->GetWeaponID() ) + { + case TF_WEAPON_ROCKETLAUNCHER: + case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT: + case TF_WEAPON_GRENADELAUNCHER: + case TF_WEAPON_PIPEBOMBLAUNCHER: + case TF_WEAPON_SCATTERGUN: + return true; + }; + } + + return false; +} + + +//----------------------------------------------------------------------------------------------------- +// Return true if given weapon doesn't make much sound when used (ie: spy knife, etc) +bool CTFBot::IsQuietWeapon( CTFWeaponBase *weapon ) const +{ + if ( weapon == MY_CURRENT_GUN ) + { + weapon = m_Shared.GetActiveTFWeapon(); + } + + if ( weapon ) + { + switch ( weapon->GetWeaponID() ) + { + case TF_WEAPON_KNIFE: + case TF_WEAPON_FISTS: + case TF_WEAPON_PDA: + case TF_WEAPON_PDA_ENGINEER_BUILD: + case TF_WEAPON_PDA_ENGINEER_DESTROY: + case TF_WEAPON_PDA_SPY: + case TF_WEAPON_BUILDER: + case TF_WEAPON_MEDIGUN: + case TF_WEAPON_DISPENSER: + case TF_WEAPON_INVIS: + case TF_WEAPON_FLAREGUN: + case TF_WEAPON_LUNCHBOX: + case TF_WEAPON_JAR: + case TF_WEAPON_COMPOUND_BOW: + case TF_WEAPON_SWORD: + case TF_WEAPON_CROSSBOW: + return true; + }; + } + + return false; +} + + +//----------------------------------------------------------------------------------------------------- +// Return true if a weapon has no obstructions along the line between the given points +bool CTFBot::IsLineOfFireClear( const Vector &from, const Vector &to ) const +{ + trace_t trace; + NextBotTraceFilterIgnoreActors botFilter( NULL, COLLISION_GROUP_NONE ); + CTraceFilterIgnoreFriendlyCombatItems ignoreFriendlyCombatFilter( this, COLLISION_GROUP_NONE, GetTeamNumber() ); + CTraceFilterChain filter( &botFilter, &ignoreFriendlyCombatFilter ); + + UTIL_TraceLine( from, to, MASK_SOLID_BRUSHONLY, &filter, &trace ); + + return !trace.DidHit(); +} + + +//----------------------------------------------------------------------------------------------------- +// Return true if a weapon has no obstructions along the line from our eye to the given position +bool CTFBot::IsLineOfFireClear( const Vector &where ) const +{ + return IsLineOfFireClear( const_cast< CTFBot * >( this )->EyePosition(), where ); +} + + +//----------------------------------------------------------------------------------------------------- +// Return true if a weapon has no obstructions along the line between the given point and entity +bool CTFBot::IsLineOfFireClear( const Vector &from, CBaseEntity *who ) const +{ + trace_t trace; + NextBotTraceFilterIgnoreActors botFilter( NULL, COLLISION_GROUP_NONE ); + CTraceFilterIgnoreFriendlyCombatItems ignoreFriendlyCombatFilter( this, COLLISION_GROUP_NONE, GetTeamNumber() ); + CTraceFilterChain filter( &botFilter, &ignoreFriendlyCombatFilter ); + + UTIL_TraceLine( from, who->WorldSpaceCenter(), MASK_SOLID_BRUSHONLY, &filter, &trace ); + + return !trace.DidHit() || trace.m_pEnt == who; +} + + +//----------------------------------------------------------------------------------------------------- +// Return true if a weapon has no obstructions along the line from our eye to the given entity +bool CTFBot::IsLineOfFireClear( CBaseEntity *who ) const +{ + return IsLineOfFireClear( const_cast< CTFBot * >( this )->EyePosition(), who ); +} + + +//----------------------------------------------------------------------------------------------------- +bool CTFBot::IsEntityBetweenTargetAndSelf( CBaseEntity *other, CBaseEntity *target ) +{ + Vector toTarget = target->GetAbsOrigin() - GetAbsOrigin(); + float rangeToTarget = toTarget.NormalizeInPlace(); + + Vector toOther = other->GetAbsOrigin() - GetAbsOrigin(); + float rangeToOther = toOther.NormalizeInPlace(); + + return rangeToOther < rangeToTarget && DotProduct( toTarget, toOther ) > 0.7071f; +} + + +//----------------------------------------------------------------------------------------------------- +// Return true if we are sure this player actually is an enemy spy +bool CTFBot::IsKnownSpy( CTFPlayer *player ) const +{ + for( int i=0; i<m_knownSpyVector.Count(); ++i ) + { + CTFPlayer *spy = m_knownSpyVector[i]; + if ( spy && player->entindex() == spy->entindex() ) + { + return true; + } + } + + return false; +} + + +//----------------------------------------------------------------------------------------------------- +// Return true if we suspect this player might be an enemy spy +CTFBot::SuspectedSpyInfo_t* CTFBot::IsSuspectedSpy( CTFPlayer *pPlayer ) +{ + for( int i=0; i<m_suspectedSpyVector.Count(); ++i ) + { + SuspectedSpyInfo_t* pSpyInfo = m_suspectedSpyVector[i]; + CTFPlayer* pSpy = pSpyInfo->m_suspectedSpy; + if ( pSpy && pPlayer->entindex() == pSpy->entindex() ) + { + return pSpyInfo; + } + } + + return NULL; +} + + +//----------------------------------------------------------------------------------------------------- +// Note that this player might be a spy +void CTFBot::SuspectSpy( CTFPlayer *pPlayer ) +{ + SuspectedSpyInfo_t* pSpyInfo = IsSuspectedSpy( pPlayer ); + + // Start suspecting this spy if we're not aware of them until now + if( pSpyInfo == NULL ) + { + // add to head for LRU effect + pSpyInfo = new SuspectedSpyInfo_t; + pSpyInfo->m_suspectedSpy = pPlayer; + m_suspectedSpyVector.AddToHead( pSpyInfo ); + } + + // Suspicious! + pSpyInfo->Suspect(); + + // Too suspicious? + if( pSpyInfo->TestForRealizing() ) + { + RealizeSpy( pPlayer ); + } +} + +void CTFBot::SuspectedSpyInfo_t::Suspect() +{ + int nCurTime = floor(gpGlobals->curtime); + + // Add our new entry + m_touchTimes.AddToHead( nCurTime ); +} + +bool CTFBot::SuspectedSpyInfo_t::TestForRealizing() +{ + // Remove any old entries + int nCurTime = floor(gpGlobals->curtime); + int nCutoffTime = nCurTime - tf_bot_suspect_spy_touch_interval.GetInt(); + + FOR_EACH_VEC_BACK( m_touchTimes, i ) + { + if( m_touchTimes[i] <= nCutoffTime ) + m_touchTimes.Remove( i ); + } + + // Add our new entry + m_touchTimes.AddToHead( nCurTime ); + + // Setup an array of bools representing the past few seconds that we want + // to look for suspicious activity + CUtlVector<bool> vecSeconds; + vecSeconds.SetSize( tf_bot_suspect_spy_touch_interval.GetInt() ); + FOR_EACH_VEC( vecSeconds, i ) + { + vecSeconds[i] = false; + } + + // Go through each time chunk and mark if there was suspicious activity + FOR_EACH_VEC( m_touchTimes, i ) + { + int nTouchTime = m_touchTimes[i]; + int nTimeSlot = nCurTime - nTouchTime; + + if( nTimeSlot >= 0 && nTimeSlot < vecSeconds.Count() ) + { + vecSeconds[nTimeSlot] = true; + } + } + + // If all are true, then the spy has been suspicious enough to warrant being realized + FOR_EACH_VEC( vecSeconds, i ) + { + if( vecSeconds[i] == false ) + { + return false; + } + } + + return true; +} + + +bool CTFBot::SuspectedSpyInfo_t::IsCurrentlySuspected() +{ + float flCutoffTime = gpGlobals->curtime - tf_bot_suspect_spy_forget_cooldown.GetFloat(); + if( m_touchTimes.Count() && m_touchTimes.Head() > flCutoffTime ) + { + return true; + } + + return false; +} + +//----------------------------------------------------------------------------------------------------- +// Note that this player *IS* a spy +void CTFBot::RealizeSpy( CTFPlayer *pPlayer ) +{ + // We already know about this spy + if ( IsKnownSpy( pPlayer ) ) + return; + + // add to head for LRU effect + m_knownSpyVector.AddToHead( pPlayer ); + + // inform my teammates + SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_CLOAKEDSPY ); + + // If I am suspicious of this spy, make everyone around me know that + // they should be suspicious too + SuspectedSpyInfo_t* pSuspectInfo = IsSuspectedSpy( pPlayer ); + if( pSuspectInfo && pSuspectInfo->IsCurrentlySuspected() ) + { + // Tell others around us we've realized there's a spy + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS ); + FOR_EACH_VEC( playerVector, i ) + { + CTFPlayer* pOther = playerVector[i]; + + if( !pOther->IsBot() ) + continue; + + //Make sure they're close by + Vector vecBetween = EyePosition() - pOther->EyePosition(); + if( vecBetween.IsLengthLessThan( 512.f ) ) + { + // If they dont know about this spy + CTFBot* pOtherBot = static_cast<CTFBot*>( pOther ); + if( !pOtherBot->IsKnownSpy( pPlayer ) ) + { + // I was suspicious that they were a spy, make my friend suspicious as well. + // This will cause them to attack a disguised spy in MvM for a bit. + pOtherBot->SuspectSpy( pPlayer ); + + // Tell them about it + pOtherBot->RealizeSpy( pPlayer ); + } + } + } + } + +} + + +//----------------------------------------------------------------------------------------------------- +// Remove player from spy suspect system +void CTFBot::ForgetSpy( CTFPlayer *pPlayer ) +{ + StopSuspectingSpy( pPlayer ); + m_knownSpyVector.FindAndFastRemove( pPlayer ); +} + +void CTFBot::StopSuspectingSpy( CTFPlayer *pPlayer ) +{ + // Find the entry matching this spy + for( int i=0; i<m_suspectedSpyVector.Count(); ++i ) + { + SuspectedSpyInfo_t* pSpyInfo = m_suspectedSpyVector[i]; + CTFPlayer* pSpy = pSpyInfo->m_suspectedSpy; + if ( pSpy && pPlayer->entindex() == pSpy->entindex() ) + { + delete pSpyInfo; + m_suspectedSpyVector.Remove(i); + break; + } + } +} + + +//----------------------------------------------------------------------------------------------------- +// Return the nearest human player on the given team who is looking directly at me +CTFPlayer *CTFBot::GetClosestHumanLookingAtMe( int team ) const +{ + CUtlVector< CTFPlayer * > otherVector; + CollectPlayers( &otherVector, team, COLLECT_ONLY_LIVING_PLAYERS ); + + float closeRange = FLT_MAX; + CTFPlayer *close = NULL; + + for( int i=0; i<otherVector.Count(); ++i ) + { + CTFPlayer *other = otherVector[i]; + + if ( other->IsBot() ) + continue; + + Vector otherEye, otherForward; + other->EyePositionAndVectors( &otherEye, &otherForward, NULL, NULL ); + + Vector toMe = const_cast< CTFBot * >( this )->EyePosition() - otherEye; + float range = toMe.NormalizeInPlace(); + + if ( range < closeRange ) + { + const float cosTolerance = 0.98f; + if ( DotProduct( toMe, otherForward ) > cosTolerance ) + { + // a human is looking toward me - check LOS + if ( IsLineOfSightClear( otherEye, IGNORE_NOTHING, other ) ) + { + close = other; + closeRange = range; + } + } + } + } + + return close; +} + + +//----------------------------------------------------------------------------------------------------- +// become a member of the given squad +void CTFBot::JoinSquad( CTFBotSquad *squad ) +{ + if ( squad ) + { + squad->Join( this ); + m_squad = squad; + } +} + + +//----------------------------------------------------------------------------------------------------- +// leave our current squad +void CTFBot::LeaveSquad( void ) +{ + if ( m_squad ) + { + m_squad->Leave( this ); + m_squad = NULL; + } +} + +//----------------------------------------------------------------------------------------------------- +// leave our current squad +void CTFBot::DeleteSquad( void ) +{ + if ( m_squad ) + { + m_squad = NULL; + } +} + +//--------------------------------------------------------------------------------------------- +bool CTFBot::IsWeaponRestricted( CTFWeaponBase *weapon ) const +{ + if ( !weapon ) + { + return false; + } + + // Get the weapon's loadout slot + CEconItemView *pEconItemView = weapon->GetAttributeContainer()->GetItem(); + if ( !pEconItemView ) + return false; + CTFItemDefinition *pItemDef = pEconItemView->GetStaticData(); + if ( !pItemDef ) + return false; + int iLoadoutSlot = pItemDef->GetLoadoutSlot( GetPlayerClass()->GetClassIndex() ); + + if ( HasWeaponRestriction( MELEE_ONLY ) ) + { + return (iLoadoutSlot != LOADOUT_POSITION_MELEE); + } + + if ( HasWeaponRestriction( PRIMARY_ONLY ) ) + { + return (iLoadoutSlot != LOADOUT_POSITION_PRIMARY); + } + + if ( HasWeaponRestriction( SECONDARY_ONLY ) ) + { + return (iLoadoutSlot != LOADOUT_POSITION_SECONDARY); + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +// +// Return true if there is something we want to reflect directly ahead of us +// +bool CTFBot::ShouldFireCompressionBlast( void ) +{ + if ( TFGameRules()->IsInTraining() ) + { + // no reflection in training mode + return false; + } + + if ( !tf_bot_pyro_always_reflect.GetBool() ) + { + if ( IsDifficulty( CTFBot::EASY ) ) + { + // easy bots can't reflect at all + return false; + } + + if ( IsDifficulty( CTFBot::NORMAL ) ) + { + // normal bots reflect some of the time + if ( TransientlyConsistentRandomValue( 1.0f ) < 0.5f ) + { + return false; + } + } + + if ( IsDifficulty( CTFBot::HARD ) ) + { + // hard bots reflect most of the time + if ( TransientlyConsistentRandomValue( 1.0f ) < 0.1f ) + { + return false; + } + } + } + + bool shouldPushPlayers = !TFGameRules()->IsMannVsMachineMode(); + + if ( shouldPushPlayers ) + { + const CKnownEntity *threat = GetVisionInterface()->GetPrimaryKnownThreat( true ); + if ( threat && threat->GetEntity() && threat->GetEntity()->IsPlayer() ) + { + CTFPlayer *pushVictim = ToTFPlayer( threat->GetEntity() ); + + if ( IsRangeLessThan( pushVictim, tf_bot_pyro_shove_away_range.GetFloat() ) ) + { + // our threat is very close - shove them! + + // always shove ubers + if ( pushVictim && pushVictim->m_Shared.IsInvulnerable() ) + { + return true; + } + + if ( pushVictim->GetGroundEntity() == NULL ) + { + // they are in the air - juggle them some of the time + return ( TransientlyConsistentRandomValue( 0.5f ) < 0.5f ); + } + + if ( pushVictim->IsCapturingPoint() ) + { + // push them off the point! + return true; + } + + // be pushy sometimes + if ( TransientlyConsistentRandomValue( 3.0f ) < 0.5f ) + { + return true; + } + } + } + } + + + Vector vecEye = EyePosition(); + Vector vecForward, vecRight, vecUp; + + AngleVectors( EyeAngles(), &vecForward, &vecRight, &vecUp ); + + Vector vecCenter = vecEye + vecForward * 128; + Vector vecSize = Vector( 128, 128, 64 ); + + const int maxCollectedEntities = 128; + CBaseEntity *pObjects[ maxCollectedEntities ]; + int count = UTIL_EntitiesInBox( pObjects, maxCollectedEntities, vecCenter - vecSize, vecCenter + vecSize, FL_CLIENT | FL_GRENADE ); + + for ( int i = 0; i < count; i++ ) + { + CBaseEntity *pObject = pObjects[i]; + if ( pObject == this ) + continue; + + if ( pObject->GetTeamNumber() == GetTeamNumber() ) + continue; + + // should air blast player logic is already done before this loop + if ( pObject->IsPlayer() ) + continue; + + // is this something I want to deflect? + if ( !pObject->IsDeflectable() ) + continue; + + if ( FClassnameIs( pObject, "tf_projectile_rocket" ) || FClassnameIs( pObject, "tf_projectile_energy_ball" ) ) + { + // is it headed right for me? + Vector vecThemUnitVel = pObject->GetAbsVelocity(); + vecThemUnitVel.z = 0.0f; + vecThemUnitVel.NormalizeInPlace(); + + Vector horzForward( vecForward.x, vecForward.y, 0.0f ); + horzForward.NormalizeInPlace(); + + if ( DotProduct( horzForward, vecThemUnitVel ) > -tf_bot_pyro_deflect_tolerance.GetFloat() ) + continue; + } + + // can I see it? + if ( !GetVisionInterface()->IsLineOfSightClear( pObject->WorldSpaceCenter() ) ) + continue; + + // bounce it! + return true; + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +// Compute a pseudo random value (0-1) that stays consistent for the +// given period of time, but changes unpredictably each period. +float CTFBot::TransientlyConsistentRandomValue( float period, int seedValue ) const +{ + CNavArea *area = GetLastKnownArea(); + if ( !area ) + { + return 0.0f; + } + + // this term stays stable for 'period' seconds, then changes in an unpredictable way + int timeMod = (int)( gpGlobals->curtime / period ) + 1; + return fabs( FastCos( (float)( seedValue + ( entindex() * area->GetID() * timeMod ) ) ) ); +} + + +//--------------------------------------------------------------------------------------------- +// Given a target entity, find a target within 'maxSplashRadius' that has clear line of fire +// to both the target entity and to me. +bool CTFBot::FindSplashTarget( CBaseEntity *target, float maxSplashRadius, Vector *splashTarget ) const +{ + if ( !target || !splashTarget ) + return false; + + *splashTarget = target->WorldSpaceCenter(); + + const int retryCount = 50; + for( int i=0; i<retryCount; ++i ) + { + Vector probe = target->WorldSpaceCenter() + RandomVector( -maxSplashRadius, maxSplashRadius ); + + trace_t trace; + NextBotTraceFilterIgnoreActors filter( NULL, COLLISION_GROUP_NONE ); + + UTIL_TraceLine( target->WorldSpaceCenter(), probe, MASK_SOLID_BRUSHONLY, &filter, &trace ); + if ( trace.DidHitWorld() ) + { + // can we shoot this spot? + if ( IsLineOfFireClear( trace.endpos ) ) + { + // yes, found a corner-sticky target + *splashTarget = trace.endpos; + + NDebugOverlay::Line( target->WorldSpaceCenter(), trace.endpos, 255, 0, 0, true, 60.0f ); + NDebugOverlay::Cross3D( trace.endpos, 5.0f, 255, 255, 0, true, 60.0f ); + + return true; + } + } + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +// Restrict bot's attention to only this entity (or radius around this entity) to the exclusion of everything else +void CTFBot::SetAttentionFocus( CBaseEntity *focusOn ) +{ + m_attentionFocusEntity = focusOn; +} + + +//--------------------------------------------------------------------------------------------- +// Remove attention focus restrictions +void CTFBot::ClearAttentionFocus( void ) +{ + m_attentionFocusEntity = NULL; +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBot::IsAttentionFocused( void ) const +{ + return m_attentionFocusEntity != NULL; +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBot::IsAttentionFocusedOn( CBaseEntity *who ) const +{ + if ( m_attentionFocusEntity == NULL || who == NULL ) + { + return false; + } + + if ( m_attentionFocusEntity->entindex() == who->entindex() ) + { + // specifically focused on this entity + return true; + } + + CTFBotActionPoint *actionPoint = dynamic_cast< CTFBotActionPoint * >( m_attentionFocusEntity.Get() ); + if ( actionPoint ) + { + // we attend to everything within the action point's radius + return actionPoint->IsWithinRange( who ); + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +// Notice the given threat after the given number of seconds have elapsed +void CTFBot::DelayedThreatNotice( CHandle< CBaseEntity > who, float noticeDelay ) +{ + float when = gpGlobals->curtime + noticeDelay; + + // if we already have a delayed notice for this threat, ignore the new one unless the delay is less + for( int i=0; i<m_delayedNoticeVector.Count(); ++i ) + { + if ( m_delayedNoticeVector[i].m_who == who ) + { + if ( m_delayedNoticeVector[i].m_when > when ) + { + // update delay to shorter time + m_delayedNoticeVector[i].m_when = when; + } + return; + } + } + + // new notice + DelayedNoticeInfo delay; + delay.m_who = who; + delay.m_when = when; + m_delayedNoticeVector.AddToTail( delay ); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBot::UpdateDelayedThreatNotices( void ) +{ + for( int i=0; i<m_delayedNoticeVector.Count(); ++i ) + { + if ( m_delayedNoticeVector[i].m_when <= gpGlobals->curtime ) + { + // delay is up - notice this threat + CBaseEntity *who = m_delayedNoticeVector[i].m_who; + + if ( who ) + { + if ( who->IsPlayer() ) + { + CTFPlayer *player = ToTFPlayer( who ); + if ( player->IsPlayerClass( TF_CLASS_SPY ) ) + { + RealizeSpy( player ); + } + } + + GetVisionInterface()->AddKnownEntity( who ); + } + + m_delayedNoticeVector.Remove( i ); + --i; + } + } +} + + +//--------------------------------------------------------------------------------------------- +void CTFBot::GiveRandomItem( loadout_positions_t loadoutPosition ) +{ + CUtlVector< const CEconItemDefinition * > itemVector; + + const CEconItemSchema::ItemDefinitionMap_t& mapItemDefs = ItemSystem()->GetItemSchema()->GetItemDefinitionMap(); + FOR_EACH_MAP_FAST( mapItemDefs, i ) + { + const CTFItemDefinition *pItemDef = dynamic_cast< const CTFItemDefinition * >( mapItemDefs[i] ); + + if ( pItemDef && pItemDef->GetLoadoutSlot( GetPlayerClass()->GetClassIndex() ) == loadoutPosition ) + { + itemVector.AddToTail( pItemDef ); + } + } + + if ( itemVector.Count() > 0 ) + { + int which = RandomInt( 0, itemVector.Count()-1 ); + +/* + CBaseCombatWeapon *myMelee = me->Weapon_GetSlot( TF_WPN_TYPE_MELEE ); + me->Weapon_Detach( myMelee ); + UTIL_Remove( myMelee ); +*/ + + const char *itemName = itemVector[ which ]->GetDefinitionName(); + BotGenerateAndWearItem( this, itemName ); + } +} + + +//--------------------------------------------------------------------------------------------- +bool CTFBot::IsSquadmate( CTFPlayer *who ) const +{ + if ( !m_squad || !who || !who->IsBotOfType( TF_BOT_TYPE ) ) + return false; + + return GetSquad() == ToTFBot( who )->GetSquad(); +} + + +//--------------------------------------------------------------------------------------------- +// Set Spy disguise to be a class that someone on the enemy team is actually using +void CTFBot::DisguiseAsMemberOfEnemyTeam( void ) +{ + CUtlVector< CTFPlayer * > enemyVector; + CollectPlayers( &enemyVector, GetEnemyTeam( GetTeamNumber() ) ); + + int disguise = RandomInt( TF_FIRST_NORMAL_CLASS, TF_LAST_NORMAL_CLASS-1 ); + + if ( enemyVector.Count() > 0 ) + { + disguise = enemyVector[ RandomInt( 0, enemyVector.Count()-1 ) ]->GetPlayerClass()->GetClassIndex(); + } + + m_Shared.Disguise( GetEnemyTeam( GetTeamNumber() ), disguise ); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBot::ClearTags( void ) +{ + m_tags.RemoveAll(); +} + + +//--------------------------------------------------------------------------------------------- +void CTFBot::AddTag( const char *tag ) +{ + if ( !HasTag( tag ) ) + { + m_tags.AddToTail( CFmtStr( "%s", tag ) ); + } +} + + +//--------------------------------------------------------------------------------------------- +void CTFBot::RemoveTag( const char *tag ) +{ + for ( int i=0; i<m_tags.Count(); ++i ) + { + if ( FStrEq( tag, m_tags[i] ) ) + { + m_tags.Remove(i); + return; + } + } +} + + +//--------------------------------------------------------------------------------------------- +// TODO: Make this an efficient lookup/match +bool CTFBot::HasTag( const char *tag ) +{ + for( int i=0; i<m_tags.Count(); ++i ) + { + if ( FStrEq( tag, m_tags[i] ) ) + { + return true; + } + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +CBaseObject *CTFBot::GetNearestKnownSappableTarget( void ) +{ + CUtlVector< CKnownEntity > knownVector; + GetVisionInterface()->CollectKnownEntities( &knownVector ); + + CBaseObject *closeObject = NULL; + float closeObjectRangeSq = 500.0f * 500.0f; + + for( int i=0; i<knownVector.Count(); ++i ) + { + CBaseObject *enemyObject = dynamic_cast< CBaseObject * >( knownVector[i].GetEntity() ); + if ( enemyObject && !enemyObject->HasSapper() && IsEnemy( enemyObject ) ) + { + float rangeSq = GetRangeSquaredTo( enemyObject ); + if ( rangeSq < closeObjectRangeSq ) + { + closeObjectRangeSq = rangeSq; + closeObject = enemyObject; + } + } + } + + return closeObject; +} + + +//----------------------------------------------------------------------------------------- +Action< CTFBot > *CTFBot::OpportunisticallyUseWeaponAbilities( void ) +{ + if ( !m_opportunisticTimer.IsElapsed() ) + { + return NULL; + } + + m_opportunisticTimer.Start( RandomFloat( 0.1f, 0.2f ) ); + + + // if I'm wearing a charge shield, use it! + if ( IsPlayerClass( TF_CLASS_DEMOMAN ) && m_Shared.IsShieldEquipped() ) + { + Vector forward; + EyeVectors( &forward ); + bool bShouldCharge = GetLocomotionInterface()->IsPotentiallyTraversable( GetAbsOrigin(), GetAbsOrigin() + 100.0f * forward, ILocomotion::IMMEDIATELY ); + if ( HasAttribute( CTFBot::AIR_CHARGE_ONLY ) && ( GetGroundEntity() || GetAbsVelocity().z > 0 ) ) + { + bShouldCharge = false; + } + + if ( bShouldCharge ) + { + PressAltFireButton(); + } + } + // if I'm wearing parachute, check if I should activate my parachute + else if ( m_Shared.IsParachuteEquipped() ) + { + bool bIsBurning = m_Shared.InCond( TF_COND_BURNING ); + float flHealthPercent = (float)GetHealth() / GetMaxHealth(); + const float flHealthThreshold = 0.5f; + // should I activate parachute? + if ( !m_Shared.InCond( TF_COND_PARACHUTE_DEPLOYED ) ) + { + float flMinParachuteGroundDistance = 300.f; + // check if I'm falling, high enough off the ground to deploy parachute, and not burning + if ( flHealthPercent >= flHealthThreshold && !bIsBurning && GetAbsVelocity().z < 0 && GetLocomotionInterface()->IsPotentiallyTraversable( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, -flMinParachuteGroundDistance ), ILocomotion::IMMEDIATELY ) ) + { + PressJumpButton(); + } + } + // should I deactivate parachute? + else + { + float flCancelParachuteDistance = 150.f; + // if I'm burning or close enough to landing, deactivate the parachute or health less than some threshold + if ( flHealthPercent < flHealthThreshold || bIsBurning || !GetLocomotionInterface()->IsPotentiallyTraversable( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, -flCancelParachuteDistance ), ILocomotion::IMMEDIATELY ) ) + { + PressJumpButton(); + } + } + } + + // don't use items if we have the flag, since most of them are unusable (unless we're a bomb carrier in MvM) + if ( HasTheFlag() && !TFGameRules()->IsMannVsMachineMode() ) + { + return NULL; + } + + for ( int w=0; w<MAX_WEAPONS; ++w ) + { + CTFWeaponBase *weapon = ( CTFWeaponBase * )GetWeapon( w ); + if ( !weapon ) + continue; + + // if I have some kind of buff banner - use it! + if ( weapon->GetWeaponID() == TF_WEAPON_BUFF_ITEM ) + { + CTFBuffItem *buff = (CTFBuffItem *)weapon; + if ( buff->IsFull() ) + { + return new CTFBotUseItem( buff ); + } + } + else if ( weapon->GetWeaponID() == TF_WEAPON_LUNCHBOX ) + { + // if we have an eatable (drink, sandvich, etc) - eat it! + CTFLunchBox *lunchbox = (CTFLunchBox *)weapon; + if ( lunchbox->HasAmmo() ) + { + // scout lunchboxes are also gated by their energy drink meter + if ( !IsPlayerClass( TF_CLASS_SCOUT ) || m_Shared.GetScoutEnergyDrinkMeter() >= 100 ) + { + return new CTFBotUseItem( lunchbox ); + } + } + } + else if ( weapon->GetWeaponID() == TF_WEAPON_BAT_WOOD ) + { + // sandman + if ( GetAmmoCount( TF_AMMO_GRENADES1 ) > 0 ) + { + const CKnownEntity *threat = GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->IsVisibleInFOVNow() ) + { + // hit a stunball + PressAltFireButton(); + } + } + } + } + + return NULL; +} + + +//----------------------------------------------------------------------------------------- +// mostly for MvM - pick a random enemy player that is not in their spawn room +CTFPlayer *CTFBot::SelectRandomReachableEnemy( void ) +{ + CUtlVector< CTFPlayer * > livePlayerVector; + CollectPlayers( &livePlayerVector, GetEnemyTeam( GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS ); + + // only consider players who have left their spawn + CUtlVector< CTFPlayer * > playerVector; + for( int i=0; i<livePlayerVector.Count(); ++i ) + { + CTFPlayer *player = livePlayerVector[i]; + if ( !PointInRespawnRoom( player, player->WorldSpaceCenter() ) ) + { + playerVector.AddToTail( player ); + } + } + + if ( playerVector.Count() > 0 ) + { + return playerVector[ RandomInt( 0, playerVector.Count()-1 ) ]; + } + + return NULL; +} + + +//----------------------------------------------------------------------------------------- +// Different sized bots used different lookahead distances +float CTFBot::GetDesiredPathLookAheadRange( void ) const +{ + return tf_bot_path_lookahead_range.GetFloat() * GetModelScale(); +} + +//----------------------------------------------------------------------------------------- +// Hack to apply idle loop sounds in MvM +void CTFBot::StartIdleSound( void ) +{ + StopIdleSound(); + + if ( TFGameRules() && !TFGameRules()->IsMannVsMachineMode() ) + return; + + // SHIELD YOUR EYES MIKEB!!! + if ( IsMiniBoss() ) + { + const char *pszSoundName = NULL; + + int iClass = GetPlayerClass()->GetClassIndex(); + switch ( iClass ) + { + case TF_CLASS_HEAVYWEAPONS: + { + pszSoundName = "MVM.GiantHeavyLoop"; + break; + } + case TF_CLASS_SOLDIER: + { + pszSoundName = "MVM.GiantSoldierLoop"; + break; + } + case TF_CLASS_DEMOMAN: + { + if ( m_mission == MISSION_DESTROY_SENTRIES ) + { + pszSoundName = "MVM.SentryBusterLoop"; + } + else + { + pszSoundName = "MVM.GiantDemomanLoop"; + } + break; + } + case TF_CLASS_SCOUT: + { + pszSoundName = "MVM.GiantScoutLoop"; + break; + } + case TF_CLASS_PYRO: + { + pszSoundName = "MVM.GiantPyroLoop"; + break; + } + } + + if ( pszSoundName ) + { + CReliableBroadcastRecipientFilter filter; + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + m_pIdleSound = controller.SoundCreate( filter, entindex(), pszSoundName ); + controller.Play( m_pIdleSound, 1.0, 100 ); + } + } +} + +//----------------------------------------------------------------------------------------- +void CTFBot::StopIdleSound( void ) +{ + if ( m_pIdleSound ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pIdleSound ); + m_pIdleSound = NULL; + } +} + +bool CTFBot::ShouldAutoJump() +{ + if ( !HasAttribute( CTFBot::AUTO_JUMP ) ) + return false; + + if ( !m_autoJumpTimer.HasStarted() ) + { + m_autoJumpTimer.Start( RandomFloat( m_flAutoJumpMin, m_flAutoJumpMax ) ); + return true; + } + else if ( m_autoJumpTimer.IsElapsed() ) + { + m_autoJumpTimer.Start( RandomFloat( m_flAutoJumpMin, m_flAutoJumpMax ) ); + return true; + } + + return false; +} + + +void CTFBot::SetFlagTarget( CCaptureFlag* pFlag ) +{ + if ( m_hFollowingFlagTarget != pFlag ) + { + if ( m_hFollowingFlagTarget ) + { + m_hFollowingFlagTarget->RemoveFollower( this ); + } + + m_hFollowingFlagTarget = pFlag; + if ( m_hFollowingFlagTarget ) + { + m_hFollowingFlagTarget->AddFollower( this ); + } + } +} + + +int CTFBot::DrawDebugTextOverlays(void) +{ + int offset = tf_bot_debug_tags.GetBool() ? 1 : BaseClass::DrawDebugTextOverlays(); + + CUtlString strTags = "Tags : "; + for( int i=0; i<m_tags.Count(); ++i ) + { + strTags.Append( m_tags[i] ); + strTags.Append( " " ); + } + + EntityText( offset, strTags.Get(), 0 ); + offset++; + + return offset; +} + + +void CTFBot::AddEventChangeAttributes( const CTFBot::EventChangeAttributes_t* newEvent ) +{ + m_eventChangeAttributes.AddToTail( newEvent ); +} + + +const CTFBot::EventChangeAttributes_t* CTFBot::GetEventChangeAttributes( const char* pszEventName ) const +{ + for ( int i=0; i<m_eventChangeAttributes.Count(); ++i ) + { + if ( FStrEq( m_eventChangeAttributes[i]->m_eventName, pszEventName ) ) + { + return m_eventChangeAttributes[i]; + } + } + return NULL; +} + + +void CTFBot::OnEventChangeAttributes( const CTFBot::EventChangeAttributes_t* pEvent ) +{ + if ( pEvent ) + { + SetDifficulty( pEvent->m_skill ); + + ClearWeaponRestrictions(); + SetWeaponRestriction( pEvent->m_weaponRestriction ); + + SetMission( pEvent->m_mission ); + + ClearAllAttributes(); + SetAttribute( pEvent->m_attributeFlags ); + + SetMaxVisionRangeOverride( pEvent->m_maxVisionRange ); + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + SetAttribute( CTFBot::BECOME_SPECTATOR_ON_DEATH ); + SetAttribute( CTFBot::RETAIN_BUILDINGS ); + } + + // cache off health value before we clear attribute because ModifyMaxHealth adds new attribute and reset the health + int nHealth = GetHealth(); + int nMaxHealth = GetMaxHealth(); + + // remove any player attributes + RemovePlayerAttributes( false ); + // and add ones that we want specifically + FOR_EACH_VEC( pEvent->m_characterAttributes, i ) + { + const CEconItemAttributeDefinition *pDef = pEvent->m_characterAttributes[i].GetAttributeDefinition(); + if ( pDef ) + { + Assert( GetAttributeList() ); + GetAttributeList()->SetRuntimeAttributeValue( pDef, pEvent->m_characterAttributes[i].m_value.asFloat ); + } + } + NetworkStateChanged(); + + // set health back to what it was before we clear bot's attributes + ModifyMaxHealth( nMaxHealth ); + SetHealth( nHealth ); + + // give items to bot before apply attribute changes + FOR_EACH_VEC( pEvent->m_items, i ) + { + AddItem( pEvent->m_items[i] ); + } + + // add attributes to equipped items + FOR_EACH_VEC( pEvent->m_itemsAttributes, i ) + { + const CTFBot::EventChangeAttributes_t::item_attributes_t& itemAttributes = pEvent->m_itemsAttributes[i]; + CSchemaItemDefHandle itemDef( itemAttributes.m_itemName ); + if ( !itemDef ) + { + Warning( "Unable to find item %s to update attribute.\n", itemAttributes.m_itemName.Get() ); + } + + for ( int iItemSlot = LOADOUT_POSITION_PRIMARY ; iItemSlot < CLASS_LOADOUT_POSITION_COUNT ; iItemSlot++ ) + { + CEconEntity* pEntity = NULL; + CEconItemView *pCurItemData = CTFPlayerSharedUtils::GetEconItemViewByLoadoutSlot( this, iItemSlot, &pEntity ); + if ( pCurItemData && itemDef && ( pCurItemData->GetItemDefIndex() == itemDef->GetDefinitionIndex() ) ) + { + for ( int iAtt=0; iAtt<itemAttributes.m_attributes.Count(); ++iAtt ) + { + const static_attrib_t& attrib = itemAttributes.m_attributes[iAtt]; + CAttributeList *pAttribList = pCurItemData->GetAttributeList(); + if ( pAttribList ) + { + pAttribList->SetRuntimeAttributeValue( attrib.GetAttributeDefinition(), attrib.m_value.asFloat ); + } + } + + if ( pEntity ) + { + // update model incase we change style + pEntity->UpdateModelToClass(); + } + + // move on to the next set of attributes + break; + } + } // for each slot + } // for each set of attributes + + // tags + ClearTags(); + for( int g=0; g<pEvent->m_tags.Count(); ++g ) + { + AddTag( pEvent->m_tags[g] ); + } + } +} + + +void CTFBot::AddItem( const char* pszItemName ) +{ + CItemSelectionCriteria criteria; + criteria.SetQuality( AE_USE_SCRIPT_VALUE ); + criteria.BAddCondition( "name", k_EOperator_String_EQ, pszItemName, true ); + + CBaseEntity *pItem = ItemGeneration()->GenerateRandomItem( &criteria, WorldSpaceCenter(), vec3_angle ); + if ( pItem ) + { + CEconItemView *pScriptItem = static_cast< CBaseCombatWeapon * >( pItem )->GetAttributeContainer()->GetItem(); + + // If we already have an item in that slot, remove it + int iClass = GetPlayerClass()->GetClassIndex(); + int iSlot = pScriptItem->GetStaticData()->GetLoadoutSlot( iClass ); + equip_region_mask_t unNewItemRegionMask = pScriptItem->GetItemDefinition() ? pScriptItem->GetItemDefinition()->GetEquipRegionConflictMask() : 0; + + if ( IsWearableSlot( iSlot ) ) + { + // Remove any wearable that has a conflicting equip_region + for ( int wbl = 0; wbl < GetNumWearables(); wbl++ ) + { + CEconWearable *pWearable = GetWearable( wbl ); + if ( !pWearable ) + continue; + + equip_region_mask_t unWearableRegionMask = 0; + if ( pWearable->GetAttributeContainer()->GetItem() ) + { + unWearableRegionMask = pWearable->GetAttributeContainer()->GetItem()->GetItemDefinition()->GetEquipRegionConflictMask(); + } + + if ( unWearableRegionMask & unNewItemRegionMask ) + { + RemoveWearable( pWearable ); + } + } + } + else + { + CBaseEntity *pEntity = GetEntityForLoadoutSlot( iSlot ); + if ( pEntity ) + { + CBaseCombatWeapon *pWpn = dynamic_cast< CBaseCombatWeapon * >( pEntity ); + Weapon_Detach( pWpn ); + UTIL_Remove( pEntity ); + } + } + + // Fake global id + pScriptItem->SetItemID( 1 ); + + DispatchSpawn( pItem ); + + CEconEntity *pNewItem = assert_cast<CEconEntity*>( pItem ); + if ( pNewItem ) + { + pNewItem->GiveTo( this ); + } + + PostInventoryApplication(); + } + else + { + if ( pszItemName && pszItemName[0] ) + { + DevMsg( "CTFBotSpawner::AddItemToBot: Invalid item %s.\n", pszItemName ); + } + } +} + + +int CTFBot::GetUberHealthThreshold() +{ + int iUberHealthThreshold = 0; + CALL_ATTRIB_HOOK_INT( iUberHealthThreshold, bot_medic_uber_health_threshold ); + if ( iUberHealthThreshold > 0 ) + { + return iUberHealthThreshold; + } + + return 50; +} + + +float CTFBot::GetUberDeployDelayDuration() +{ + float flDelayUberDuration = 0; + CALL_ATTRIB_HOOK_INT( flDelayUberDuration, bot_medic_uber_deploy_delay_duration ); + if ( flDelayUberDuration > 0 ) + { + return flDelayUberDuration; + } + + return -1.f; +} diff --git a/game/server/tf/bot/tf_bot.h b/game/server/tf/bot/tf_bot.h new file mode 100644 index 0000000..62be222 --- /dev/null +++ b/game/server/tf/bot/tf_bot.h @@ -0,0 +1,1080 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot.h +// Team Fortress NextBot +// Michael Booth, February 2009 + +#ifndef TF_BOT_H +#define TF_BOT_H + +#include "Player/NextBotPlayer.h" +#include "../nav_mesh/tf_nav_mesh.h" +#include "tf_bot_vision.h" +#include "tf_bot_body.h" +#include "tf_bot_locomotion.h" +#include "tf_player.h" +#include "tf_bot_squad.h" +#include "bot/map_entities/tf_bot_proxy.h" +#include "tf_gamerules.h" +#include "entity_capture_flag.h" +#include "func_capture_zone.h" +#include "nav_entities.h" +#include "utlstack.h" + +#define TF_BOT_TYPE 1337 + +class CTriggerAreaCapture; +class CTFBotActionPoint; +class CObjectSentrygun; +class CTFBotGenerator; + +extern void BotGenerateAndWearItem( CTFPlayer *pBot, const char *itemName ); + +//---------------------------------------------------------------------------- +// These must remain in sync with the bot_generator's spawnflags in tf.fgd: +#define TFBOT_IGNORE_ENEMY_SCOUTS 0x0001 +#define TFBOT_IGNORE_ENEMY_SOLDIERS 0x0002 +#define TFBOT_IGNORE_ENEMY_PYROS 0x0004 +#define TFBOT_IGNORE_ENEMY_DEMOMEN 0x0008 +#define TFBOT_IGNORE_ENEMY_HEAVIES 0x0010 +#define TFBOT_IGNORE_ENEMY_MEDICS 0x0020 +#define TFBOT_IGNORE_ENEMY_ENGINEERS 0x0040 +#define TFBOT_IGNORE_ENEMY_SNIPERS 0x0080 +#define TFBOT_IGNORE_ENEMY_SPIES 0x0100 +#define TFBOT_IGNORE_ENEMY_SENTRY_GUNS 0x0200 +#define TFBOT_IGNORE_SCENARIO_GOALS 0x0400 + +#define TFBOT_ALL_BEHAVIOR_FLAGS 0xFFFF + +#define TFBOT_MVM_MAX_PATH_LENGTH 0.0f // 7000.0f // in MvM, all pathfinds are limited to this (0 == no limit) + + +//---------------------------------------------------------------------------- +class CTFBot: public NextBotPlayer< CTFPlayer >, public CGameEventListener +{ +public: + DECLARE_CLASS( CTFBot, NextBotPlayer< CTFPlayer > ); + + CTFBot(); + virtual ~CTFBot(); + + virtual void Spawn(); + virtual void FireGameEvent( IGameEvent *event ); + virtual void Event_Killed( const CTakeDamageInfo &info ); + virtual void PhysicsSimulate( void ); + virtual void Touch( CBaseEntity *pOther ); + virtual void AvoidPlayers( CUserCmd *pCmd ); // some game types allow players to pass through each other, this method pushes them apart + virtual void UpdateOnRemove( void ); + virtual int ShouldTransmit( const CCheckTransmitInfo *pInfo ) OVERRIDE; + virtual void ChangeTeam( int iTeamNum, bool bAutoTeam, bool bSilent, bool bAutoBalance = false ) OVERRIDE; + virtual bool ShouldGib( const CTakeDamageInfo &info ) OVERRIDE; + + virtual int DrawDebugTextOverlays(void); + + virtual bool IsAllowedToPickUpFlag( void ) const; + + virtual void InitClass( void ); // set health/etc + void ModifyMaxHealth( int nNewMaxHealth, bool bSetCurrentHealth = true, bool bAllowModelScaling = true ); + + virtual int GetBotType( void ) const; // return a unique int representing the type of bot instance this is + + virtual CTFNavArea *GetLastKnownArea( void ) const { return static_cast< CTFNavArea * >( BaseClass::GetLastKnownArea() ); } // return the last nav area the player occupied - NULL if unknown + + // NextBotPlayer + static CBasePlayer *AllocatePlayerEntity( edict_t *pEdict, const char *playerName ); + + virtual void PressFireButton( float duration = -1.0f ) OVERRIDE; + virtual void PressAltFireButton( float duration = -1.0f ) OVERRIDE; + virtual void PressSpecialFireButton( float duration = -1.0f ) OVERRIDE; + + // INextBot + virtual CTFBotLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + virtual CTFBotBody *GetBodyInterface( void ) const { return m_body; } + virtual CTFBotVision *GetVisionInterface( void ) const { return m_vision; } + DECLARE_INTENTION_INTERFACE( CTFBot ); + + virtual bool IsDormantWhenDead( void ) const; // should this player-bot continue to update itself when dead (respawn logic, etc) + + virtual void OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ); // when someone fires their weapon + + virtual bool IsDebugFilterMatch( const char *name ) const; // return true if we match the given debug symbol + + virtual int GetAllowedTauntPartnerTeam() const OVERRIDE { return GetTeamNumber(); } + + // CTFBot specific + CTeamControlPoint *GetMyControlPoint( void ) const; // return point we want to capture, or need to defend + void ClearMyControlPoint( void ); + bool WasPointJustLost( void ) const; // return true if we just lost territory recently + bool AreAllPointsUncontestedSoFar( void ) const; // return true if no enemy has contested any point yet + bool IsPointBeingCaptured( CTeamControlPoint *point ) const; // return true if the given point is being captured + bool IsAnyPointBeingCaptured( void ) const; // return true if any point is being captured + bool IsNearPoint( CTeamControlPoint *point ) const; // return true if we are within a short travel distance of the current point + float GetTimeLeftToCapture( void ) const; // return time left to capture the point before we lose the game + + CCaptureFlag *GetFlagToFetch( void ) const; // return flag we want to fetch + CCaptureZone *GetFlagCaptureZone( void ) const; // return capture zone for our flag(s) + + struct SniperSpotInfo + { + CTFNavArea *m_vantageArea; + Vector m_vantageSpot; + + CTFNavArea *m_theaterArea; + Vector m_theaterSpot; + + float m_range; + float m_advantage; // the difference in how long it takes us to reach our vantage spot vs them to reach the theater spot + }; + + void AccumulateSniperSpots( void ); // find good sniping spots and store them + const CUtlVector< SniperSpotInfo > *GetSniperSpots( void ) const; // return vector of good sniping positions + bool HasSniperSpots( void ) const; + void ClearSniperSpots( void ); + + // search outwards from startSearchArea and collect all reachable objects from the given list that pass the given filter + void SelectReachableObjects( const CUtlVector< CHandle< CBaseEntity > > &candidateObjectVector, CUtlVector< CHandle< CBaseEntity > > *selectedObjectVector, const INextBotFilter &filter, CNavArea *startSearchArea, float maxRange = 2000.0f ) const; + CBaseEntity *FindClosestReachableObject( const char *objectName, CNavArea *from, float maxRange = 2000.0f ) const; + + CTFNavArea *GetSpawnArea( void ) const; // get area where we spawned in + + bool IsAmmoLow( void ) const; + bool IsAmmoFull( void ) const; + + void UpdateLookingAroundForEnemies( void ); // update our view to keep an eye on enemies, and where enemies come from + + #define LOOK_FOR_FRIENDS false + #define LOOK_FOR_ENEMIES true + void UpdateLookingAroundForIncomingPlayers( bool lookForEnemies ); // update our view to watch where friends or enemies will be coming from + void StartLookingAroundForEnemies( void ); // enable updating view for enemy searching + void StopLookingAroundForEnemies( void ); // disable updating view for enemy searching + + void SetAttentionFocus( CBaseEntity *focusOn ); // restrict bot's attention to only this entity (or radius around this entity) to the exclusion of everything else + void ClearAttentionFocus( void ); // remove attention focus restrictions + bool IsAttentionFocused( void ) const; + bool IsAttentionFocusedOn( CBaseEntity *who ) const; + + void DelayedThreatNotice( CHandle< CBaseEntity > who, float noticeDelay ); // notice the given threat after the given number of seconds have elapsed + void UpdateDelayedThreatNotices( void ); + + CTFNavArea *FindVantagePoint( float maxTravelDistance = 2000.0f ) const; // return a nearby area where we can see a member of the enemy team + + const char *GetNextSpawnClassname( void ) const; + + float GetThreatDanger( CBaseCombatCharacter *who ) const; // return perceived danger of threat (0=none, 1=immediate deadly danger) + float GetMaxAttackRange( void ) const; // return the max range at which we can effectively attack + float GetDesiredAttackRange( void ) const; // return the ideal range at which we can effectively attack + + bool EquipRequiredWeapon( void ); // if we're required to equip a specific weapon, do it. + void EquipBestWeaponForThreat( const CKnownEntity *threat ); // equip the best weapon we have to attack the given threat + bool EquipLongRangeWeapon( void ); // equip a weapon that can damage far-away targets + + void PushRequiredWeapon( CTFWeaponBase *weapon ); // force us to equip and use this weapon until popped off the required stack + void PopRequiredWeapon( void ); // pop top required weapon off of stack and discard + + #define MY_CURRENT_GUN NULL // can be passed as weapon to following queries + bool IsCombatWeapon( CTFWeaponBase *weapon ) const; // return true if given weapon can be used to attack + bool IsHitScanWeapon( CTFWeaponBase *weapon ) const; // return true if given weapon is a "hitscan" weapon (scattered tracelines with instant damage) + bool IsContinuousFireWeapon( CTFWeaponBase *weapon ) const; // return true if given weapon "sprays" bullets/fire/etc continuously (ie: not individual rockets/etc) + bool IsExplosiveProjectileWeapon( CTFWeaponBase *weapon ) const;// return true if given weapon launches explosive projectiles with splash damage + bool IsBarrageAndReloadWeapon( CTFWeaponBase *weapon ) const; // return true if given weapon has small clip and long reload cost (ie: rocket launcher, etc) + bool IsQuietWeapon( CTFWeaponBase *weapon ) const; // return true if given weapon doesn't make much sound when used (ie: spy knife, etc) + + bool IsEnvironmentNoisy( void ) const; // return true if there are/have been loud noises (ie: non-quiet weapons) nearby very recently + + enum WeaponRestrictionType + { + ANY_WEAPON = 0, + MELEE_ONLY = 0x0001, + PRIMARY_ONLY = 0x0002, + SECONDARY_ONLY = 0x0004, + }; + void ClearWeaponRestrictions( void ); + void SetWeaponRestriction( int restrictionFlags ); + bool HasWeaponRestriction( int restrictionFlags ) const; + bool IsWeaponRestricted( CTFWeaponBase *weapon ) const; + + bool ShouldFireCompressionBlast( void ); + + bool IsLineOfFireClear( const Vector &where ) const; // return true if a weapon has no obstructions along the line from our eye to the given position + bool IsLineOfFireClear( CBaseEntity *who ) const; // return true if a weapon has no obstructions along the line from our eye to the given entity + bool IsLineOfFireClear( const Vector &from, const Vector &to ) const; // return true if a weapon has no obstructions along the line between the given points + bool IsLineOfFireClear( const Vector &from, CBaseEntity *who ) const; // return true if a weapon has no obstructions along the line between the given point and entity + + bool IsEntityBetweenTargetAndSelf( CBaseEntity *other, CBaseEntity *target ); // return true if "other" is positioned inbetween us and "target" + + class SuspectedSpyInfo_t + { + public: + bool IsCurrentlySuspected(); + void Suspect(); // The verb form of the word, not the noun. + bool TestForRealizing(); + CHandle< CTFPlayer > m_suspectedSpy; + + private: + + + CUtlVector< int > m_touchTimes; + }; + + bool IsKnownSpy( CTFPlayer *player ) const; // return true if we are sure this player actually is an enemy spy + SuspectedSpyInfo_t* IsSuspectedSpy( CTFPlayer *player ); // return true if we suspect this player might be an enemy spy + void SuspectSpy( CTFPlayer *player ); // note that this player might be a spy + void RealizeSpy( CTFPlayer *player ); // note that this player *IS* a spy + void ForgetSpy( CTFPlayer *player ); // remove player from spy suspect system + void StopSuspectingSpy( CTFPlayer *pPlayer ); + + CTFPlayer *GetClosestHumanLookingAtMe( int team = TEAM_ANY ) const; // return the nearest human player on the given team who is looking directly at me + + enum AttributeType + { + REMOVE_ON_DEATH = 1<<0, // kick bot from server when killed + AGGRESSIVE = 1<<1, // in MvM mode, push for the cap point + IS_NPC = 1<<2, // a non-player support character + SUPPRESS_FIRE = 1<<3, + DISABLE_DODGE = 1<<4, + BECOME_SPECTATOR_ON_DEATH = 1<<5, // move bot to spectator team when killed + QUOTA_MANANGED = 1<<6, // managed by the bot quota in CTFBotManager + RETAIN_BUILDINGS = 1<<7, // don't destroy this bot's buildings when it disconnects + SPAWN_WITH_FULL_CHARGE = 1<<8, // all weapons start with full charge (ie: uber) + ALWAYS_CRIT = 1<<9, // always fire critical hits + IGNORE_ENEMIES = 1<<10, + HOLD_FIRE_UNTIL_FULL_RELOAD = 1<<11, // don't fire our barrage weapon until it is full reloaded (rocket launcher, etc) + PRIORITIZE_DEFENSE = 1<<12, // bot prioritizes defending when possible + ALWAYS_FIRE_WEAPON = 1<<13, // constantly fire our weapon + TELEPORT_TO_HINT = 1<<14, // bot will teleport to hint target instead of walking out from the spawn point + MINIBOSS = 1<<15, // is miniboss? + USE_BOSS_HEALTH_BAR = 1<<16, // should I use boss health bar? + IGNORE_FLAG = 1<<17, // don't pick up flag/bomb + AUTO_JUMP = 1<<18, // auto jump + AIR_CHARGE_ONLY = 1<<19, // demo knight only charge in the air + PREFER_VACCINATOR_BULLETS = 1<<20, // When using the vaccinator, prefer to use the bullets shield + PREFER_VACCINATOR_BLAST = 1<<21, // When using the vaccinator, prefer to use the blast shield + PREFER_VACCINATOR_FIRE = 1<<22, // When using the vaccinator, prefer to use the fire shield + BULLET_IMMUNE = 1<<23, // Has a shield that makes the bot immune to bullets + BLAST_IMMUNE = 1<<24, // "" blast + FIRE_IMMUNE = 1<<25, // "" fire + PARACHUTE = 1<<26, // demo/soldier parachute when falling + PROJECTILE_SHIELD = 1<<27, // medic projectile shield + }; + void SetAttribute( int attributeFlag ); + void ClearAttribute( int attributeFlag ); + void ClearAllAttributes(); + bool HasAttribute( int attributeFlag ) const; + + enum DifficultyType + { + UNDEFINED = -1, + EASY = 0, + NORMAL = 1, + HARD = 2, + EXPERT = 3, + + NUM_DIFFICULTY_LEVELS + }; + DifficultyType GetDifficulty( void ) const; + void SetDifficulty( DifficultyType difficulty ); + bool IsDifficulty( DifficultyType skill ) const; + + void SetHomeArea( CTFNavArea *area ); + CTFNavArea *GetHomeArea( void ) const; + + CObjectSentrygun *GetEnemySentry( void ) const; // if we've been attacked/killed by an enemy sentry, this will return it, otherwise NULL + void RememberEnemySentry( CObjectSentrygun *sentry, const Vector &injurySpot ); + const Vector &GetSpotWhereEnemySentryLastInjuredMe( void ) const; + + void SetActionPoint( CTFBotActionPoint *point ); + CTFBotActionPoint *GetActionPoint( void ) const; + + bool HasProxy( void ) const; + void SetProxy( CTFBotProxy *proxy ); // attach this bot to a bot_proxy entity for map I/O communications + CTFBotProxy *GetProxy( void ) const; + + bool HasSpawner( void ) const; + void SetSpawner( CTFBotGenerator *spawner ); + CTFBotGenerator *GetSpawner( void ) const; + + void JoinSquad( CTFBotSquad *squad ); // become a member of the given squad + void LeaveSquad( void ); // leave our current squad + void DeleteSquad( void ); + bool IsInASquad( void ) const; + bool IsSquadmate( CTFPlayer *who ) const; // return true if given bot is in my squad + CTFBotSquad *GetSquad( void ) const; // return squad we are in, or NULL + float GetSquadFormationError( void ) const; // return normalized error term where 0 = in formation position and 1 = completely out of position + void SetSquadFormationError( float error ); + bool HasBrokenFormation( void ) const; // return true if this bot is far out of formation, or has no path back + void SetBrokenFormation( bool state ); + + float TransientlyConsistentRandomValue( float period = 10.0f, int seedValue = 0 ) const; // compute a pseudo random value (0-1) that stays consistent for the given period of time, but changes unpredictably each period + + void SetBehaviorFlag( unsigned int flags ); + void ClearBehaviorFlag( unsigned int flags ); + bool IsBehaviorFlagSet( unsigned int flags ) const; + + bool FindSplashTarget( CBaseEntity *target, float maxSplashRadius, Vector *splashTarget ) const; + + void GiveRandomItem( loadout_positions_t loadoutPosition ); + + enum MissionType + { + NO_MISSION = 0, + MISSION_SEEK_AND_DESTROY, // focus on finding and killing enemy players + MISSION_DESTROY_SENTRIES, // focus on finding and destroying enemy sentry guns (and buildings) + MISSION_SNIPER, // maintain teams of snipers harassing the enemy + MISSION_SPY, // maintain teams of spies harassing the enemy + MISSION_ENGINEER, // maintain engineer nests for harassing the enemy + MISSION_REPROGRAMMED, // MvM: robot has been hacked and will do bad things to their team + }; + #define MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM false + void SetMission( MissionType mission, bool resetBehaviorSystem = true ); + void SetPrevMission( MissionType mission ); + MissionType GetMission( void ) const; + MissionType GetPrevMission( void ) const; + bool HasMission( MissionType mission ) const; + bool IsOnAnyMission( void ) const; + void SetMissionTarget( CBaseEntity *target ); + CBaseEntity *GetMissionTarget( void ) const; + void SetMissionString( CUtlString string ); + CUtlString *GetMissionString( void ); + + void SetTeleportWhere( const CUtlStringList& teleportWhereName ); + const CUtlStringList& GetTeleportWhere(); + void ClearTeleportWhere(); + + void SetScaleOverride( float fScale ); + + void SetMaxVisionRangeOverride( float range ); + float GetMaxVisionRangeOverride( void ) const; + + void DisguiseAsMemberOfEnemyTeam( void ); // set Spy disguise to be a class that someone on the enemy team is actually using + CBaseObject *GetNearestKnownSappableTarget( void ); + + void ClearTags( void ); + void AddTag( const char *tag ); + void RemoveTag( const char *tag ); + bool HasTag( const char *tag ); + + Action< CTFBot > *OpportunisticallyUseWeaponAbilities( void ); + + CTFPlayer *SelectRandomReachableEnemy( void ); // mostly for MvM - pick a random enemy player that is not in their spawn room + + float GetDesiredPathLookAheadRange( void ) const; // different sized bots used different lookahead distances + + void StartIdleSound( void ); + void StopIdleSound( void ); + + bool ShouldQuickBuild() const { return m_bForceQuickBuild; } + void SetShouldQuickBuild( bool bShouldQuickBuild ) { m_bForceQuickBuild = bShouldQuickBuild; } + + void SetAutoJump( float flAutoJumpMin, float flAutoJumpMax ) { m_flAutoJumpMin = flAutoJumpMin; m_flAutoJumpMax = flAutoJumpMax; } + bool ShouldAutoJump(); + + void SetFlagTarget( CCaptureFlag* pFlag ); + CCaptureFlag* GetFlagTarget() const { return m_hFollowingFlagTarget; } + bool HasFlagTaget() const { return m_hFollowingFlagTarget != NULL; } + + struct EventChangeAttributes_t + { + EventChangeAttributes_t() + { + Reset(); + } + + EventChangeAttributes_t( const EventChangeAttributes_t& copy ) + { + Reset(); + + m_eventName = copy.m_eventName; + + m_skill = copy.m_skill; + m_weaponRestriction = copy.m_weaponRestriction; + m_mission = copy.m_mission; + m_prevMission = copy.m_prevMission; + m_attributeFlags = copy.m_attributeFlags; + m_maxVisionRange = copy.m_maxVisionRange; + + for ( int i=0; i<copy.m_items.Count(); ++i ) + { + m_items.CopyAndAddToTail( copy.m_items[i] ); + } + + m_itemsAttributes = copy.m_itemsAttributes; + m_characterAttributes = copy.m_characterAttributes; + + for ( int i=0; i<copy.m_tags.Count(); ++i ) + { + m_tags.CopyAndAddToTail( copy.m_tags[i] ); + } + } + + void Reset() + { + m_eventName = "default"; + + m_skill = CTFBot::EASY; + m_weaponRestriction = CTFBot::ANY_WEAPON; + m_mission = CTFBot::NO_MISSION; + m_prevMission = m_mission; + m_attributeFlags = 0; + m_maxVisionRange = -1.f; + + m_items.RemoveAll(); + + m_itemsAttributes.RemoveAll(); + m_characterAttributes.RemoveAll(); + m_tags.RemoveAll(); + } + + CUtlString m_eventName; + + DifficultyType m_skill; + WeaponRestrictionType m_weaponRestriction; + MissionType m_mission; + MissionType m_prevMission; + int m_attributeFlags; + float m_maxVisionRange; + + CUtlStringList m_items; + + struct item_attributes_t + { + CUtlString m_itemName; + CCopyableUtlVector< static_attrib_t > m_attributes; + }; + CUtlVector< item_attributes_t > m_itemsAttributes; + CUtlVector< static_attrib_t > m_characterAttributes; + CUtlStringList m_tags; + }; + void ClearEventChangeAttributes() { m_eventChangeAttributes.RemoveAll(); } + void AddEventChangeAttributes( const EventChangeAttributes_t* newEvent ); + const EventChangeAttributes_t* GetEventChangeAttributes( const char* pszEventName ) const; + void OnEventChangeAttributes( const CTFBot::EventChangeAttributes_t* pEvent ); + + void AddItem( const char* pszItemName ); + + int GetUberHealthThreshold(); + float GetUberDeployDelayDuration(); + +private: + CTFBotLocomotion *m_locomotor; + CTFBotBody *m_body; + CTFBotVision *m_vision; + + CountdownTimer m_lookAtEnemyInvasionAreasTimer; + + CTFNavArea *m_spawnArea; // where we spawned + CountdownTimer m_justLostPointTimer; + + int m_weaponRestrictionFlags; + int m_attributeFlags; + DifficultyType m_difficulty; + + CTFNavArea *m_homeArea; + + CHandle< CTFBotActionPoint > m_actionPoint; + CHandle< CTFBotProxy > m_proxy; + CHandle< CTFBotGenerator > m_spawner; + + CTFBotSquad *m_squad; + bool m_didReselectClass; + + CHandle< CObjectSentrygun > m_enemySentry; + Vector m_spotWhereEnemySentryLastInjuredMe; // the last position where I was injured by an enemy sentry + + CUtlVector< SuspectedSpyInfo_t* > m_suspectedSpyVector; + CUtlVector< CHandle< CTFPlayer > > m_knownSpyVector; + + CUtlVector< SniperSpotInfo > m_sniperSpotVector; // collection of good sniping spots for the current objective + + CUtlVector< CTFNavArea * > m_sniperVantageAreaVector; + CUtlVector< CTFNavArea * > m_sniperTheaterAreaVector; + + CBaseEntity *m_snipingGoalEntity; // the entity we are guarding (control point, payload cart) + Vector m_lastSnipingGoalEntityPosition; + + void SetupSniperSpotAccumulation( void ); // do internal setup when control point changes + CountdownTimer m_retrySniperSpotSetupTimer; + + bool m_isLookingAroundForEnemies; + + unsigned int m_behaviorFlags; // spawnflags from the bot_generator that spawned us + CUtlVector< CFmtStr > m_tags; + + CHandle< CBaseEntity > m_attentionFocusEntity; + + CTeamControlPoint *SelectPointToCapture( CUtlVector< CTeamControlPoint * > *captureVector ) const; + CTeamControlPoint *SelectPointToDefend( CUtlVector< CTeamControlPoint * > *defendVector ) const; + mutable CHandle< CTeamControlPoint > m_myControlPoint; + mutable CountdownTimer m_evaluateControlPointTimer; + + float m_fModelScaleOverride; + + MissionType m_mission; + MissionType m_prevMission; + + CHandle< CBaseEntity > m_missionTarget; + CUtlString m_missionString; + + CUtlStack< CHandle<CTFWeaponBase> > m_requiredWeaponStack; // if non-empty, bot must equip the weapon on top of the stack + + CountdownTimer m_noisyTimer; + + struct DelayedNoticeInfo + { + CHandle< CBaseEntity > m_who; + float m_when; + }; + CUtlVector< DelayedNoticeInfo > m_delayedNoticeVector; + + float m_maxVisionRangeOverride; + + CountdownTimer m_opportunisticTimer; + + CSoundPatch *m_pIdleSound; + + float m_squadFormationError; + bool m_hasBrokenFormation; + + CUtlStringList m_teleportWhereName; // spawn name an engineer mission teleporter will override + bool m_bForceQuickBuild; + + float m_flAutoJumpMin; + float m_flAutoJumpMax; + CountdownTimer m_autoJumpTimer; + + CHandle< CCaptureFlag > m_hFollowingFlagTarget; + + CUtlVector< const EventChangeAttributes_t* > m_eventChangeAttributes; +}; + + +inline void CTFBot::SetTeleportWhere( const CUtlStringList& teleportWhereName ) +{ + // deep copy strings + for ( int i=0; i<teleportWhereName.Count(); ++i ) + { + m_teleportWhereName.CopyAndAddToTail( teleportWhereName[i] ); + } +} + +inline const CUtlStringList& CTFBot::GetTeleportWhere() +{ + return m_teleportWhereName; +} + +inline void CTFBot::ClearTeleportWhere() +{ + m_teleportWhereName.RemoveAll(); +} + +inline void CTFBot::SetMissionString( CUtlString string ) +{ + m_missionString = string; +} + +inline CUtlString *CTFBot::GetMissionString( void ) +{ + return &m_missionString; +} + +inline void CTFBot::SetMissionTarget( CBaseEntity *target ) +{ + m_missionTarget = target; +} + +inline CBaseEntity *CTFBot::GetMissionTarget( void ) const +{ + return m_missionTarget; +} + +inline float CTFBot::GetSquadFormationError( void ) const +{ + return m_squadFormationError; +} + +inline void CTFBot::SetSquadFormationError( float error ) +{ + m_squadFormationError = error; +} + +inline bool CTFBot::HasBrokenFormation( void ) const +{ + return m_hasBrokenFormation; +} + +inline void CTFBot::SetBrokenFormation( bool state ) +{ + m_hasBrokenFormation = state; +} + +inline void CTFBot::SetMaxVisionRangeOverride( float range ) +{ + m_maxVisionRangeOverride = range; +} + +inline float CTFBot::GetMaxVisionRangeOverride( void ) const +{ + return m_maxVisionRangeOverride; +} + +inline void CTFBot::SetBehaviorFlag( unsigned int flags ) +{ + m_behaviorFlags |= flags; +} + +inline void CTFBot::ClearBehaviorFlag( unsigned int flags ) +{ + m_behaviorFlags &= ~flags; +} + +inline bool CTFBot::IsBehaviorFlagSet( unsigned int flags ) const +{ + return ( m_behaviorFlags & flags ) ? true : false; +} + +inline void CTFBot::StartLookingAroundForEnemies( void ) +{ + m_isLookingAroundForEnemies = true; +} + +inline void CTFBot::StopLookingAroundForEnemies( void ) +{ + m_isLookingAroundForEnemies = false; +} + +inline int CTFBot::GetBotType( void ) const +{ + return TF_BOT_TYPE; +} + +inline void CTFBot::RememberEnemySentry( CObjectSentrygun *sentry, const Vector &injurySpot ) +{ + m_enemySentry = sentry; + m_spotWhereEnemySentryLastInjuredMe = injurySpot; +} + +inline CObjectSentrygun *CTFBot::GetEnemySentry( void ) const +{ + return m_enemySentry; +} + +inline const Vector &CTFBot::GetSpotWhereEnemySentryLastInjuredMe( void ) const +{ + return m_spotWhereEnemySentryLastInjuredMe; +} + +inline CTFBot::DifficultyType CTFBot::GetDifficulty( void ) const +{ + return m_difficulty; +} + +inline void CTFBot::SetDifficulty( CTFBot::DifficultyType difficulty ) +{ + m_difficulty = difficulty; + + m_nBotSkill = m_difficulty; +} + +inline bool CTFBot::IsDifficulty( DifficultyType skill ) const +{ + return skill == m_difficulty; +} + +inline bool CTFBot::HasProxy( void ) const +{ + return m_proxy == NULL ? false : true; +} + +inline void CTFBot::SetProxy( CTFBotProxy *proxy ) +{ + m_proxy = proxy; +} + +inline CTFBotProxy *CTFBot::GetProxy( void ) const +{ + return m_proxy; +} + +inline bool CTFBot::HasSpawner( void ) const +{ + return m_spawner == NULL ? false : true; +} + +inline void CTFBot::SetSpawner( CTFBotGenerator *spawner ) +{ + m_spawner = spawner; +} + +inline CTFBotGenerator *CTFBot::GetSpawner( void ) const +{ + return m_spawner; +} + +inline void CTFBot::SetActionPoint( CTFBotActionPoint *point ) +{ + m_actionPoint = point; +} + +inline CTFBotActionPoint *CTFBot::GetActionPoint( void ) const +{ + return m_actionPoint; +} + +inline bool CTFBot::IsInASquad( void ) const +{ + return m_squad == NULL ? false : true; +} + +inline CTFBotSquad *CTFBot::GetSquad( void ) const +{ + return m_squad; +} + +inline void CTFBot::SetHomeArea( CTFNavArea *area ) +{ + m_homeArea = area; +} + +inline CTFNavArea *CTFBot::GetHomeArea( void ) const +{ + return m_homeArea; +} + +inline void CTFBot::ClearWeaponRestrictions( void ) +{ + m_weaponRestrictionFlags = 0; +} + +inline void CTFBot::SetWeaponRestriction( int restrictionFlags ) +{ + m_weaponRestrictionFlags |= restrictionFlags; +} + +inline bool CTFBot::HasWeaponRestriction( int restrictionFlags ) const +{ + return m_weaponRestrictionFlags & restrictionFlags ? true : false; +} + +inline void CTFBot::SetAttribute( int attributeFlag ) +{ + m_attributeFlags |= attributeFlag; +} + +inline void CTFBot::ClearAttribute( int attributeFlag ) +{ + m_attributeFlags &= ~attributeFlag; +} + +inline void CTFBot::ClearAllAttributes() +{ + m_attributeFlags = 0; +} + +inline bool CTFBot::HasAttribute( int attributeFlag ) const +{ + return m_attributeFlags & attributeFlag ? true : false; +} + +inline CTFNavArea *CTFBot::GetSpawnArea( void ) const +{ + return m_spawnArea; +} + +inline bool CTFBot::WasPointJustLost( void ) const +{ + return m_justLostPointTimer.HasStarted() && !m_justLostPointTimer.IsElapsed(); +} + +inline const CUtlVector< CTFBot::SniperSpotInfo > *CTFBot::GetSniperSpots( void ) const +{ + return &m_sniperSpotVector; +} + +inline bool CTFBot::HasSniperSpots( void ) const +{ + return m_sniperSpotVector.Count() > 0 ? true : false; +} + +inline CTFBot::MissionType CTFBot::GetMission( void ) const +{ + return m_mission; +} + +inline void CTFBot::SetPrevMission( MissionType mission ) +{ + m_prevMission = mission; +} + +inline CTFBot::MissionType CTFBot::GetPrevMission( void ) const +{ + return m_prevMission; +} + +inline bool CTFBot::HasMission( MissionType mission ) const +{ + return m_mission == mission ? true : false; +} + +inline bool CTFBot::IsOnAnyMission( void ) const +{ + return m_mission == NO_MISSION ? false : true; +} + +inline void CTFBot::SetScaleOverride( float fScale ) +{ + m_fModelScaleOverride = fScale; + + SetModelScale( m_fModelScaleOverride > 0.0f ? m_fModelScaleOverride : 1.0f ); +} + +inline bool CTFBot::IsEnvironmentNoisy( void ) const +{ + return !m_noisyTimer.IsElapsed(); +} + +//--------------------------------------------------------------------------------------------- +inline CTFBot *ToTFBot( CBaseEntity *pEntity ) +{ + if ( !pEntity || !pEntity->IsPlayer() || !ToTFPlayer( pEntity )->IsBotOfType( TF_BOT_TYPE ) ) + return NULL; + + Assert( "***IMPORTANT!!! DONT IGNORE ME!!!***" && dynamic_cast< CTFBot * >( pEntity ) != 0 ); + + return static_cast< CTFBot * >( pEntity ); +} + + +//--------------------------------------------------------------------------------------------- +inline const CTFBot *ToTFBot( const CBaseEntity *pEntity ) +{ + if ( !pEntity || !pEntity->IsPlayer() || !ToTFPlayer( const_cast< CBaseEntity * >( pEntity ) )->IsBotOfType( TF_BOT_TYPE ) ) + return NULL; + + Assert( "***IMPORTANT!!! DONT IGNORE ME!!!***" && dynamic_cast< const CTFBot * >( pEntity ) != 0 ); + + return static_cast< const CTFBot * >( pEntity ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Functor used with NavAreaBuildPath() + */ +class CTFBotPathCost : public IPathCost +{ +public: + CTFBotPathCost( CTFBot *me, RouteType routeType ) + { + m_me = me; + m_routeType = routeType; + m_stepHeight = me->GetLocomotionInterface()->GetStepHeight(); + m_maxJumpHeight = me->GetLocomotionInterface()->GetMaxJumpHeight(); + m_maxDropHeight = me->GetLocomotionInterface()->GetDeathDropHeight(); + } + + virtual float operator()( CNavArea *baseArea, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const + { + VPROF_BUDGET( "CTFBotPathCost::operator()", "NextBot" ); + + CTFNavArea *area = (CTFNavArea *)baseArea; + + if ( fromArea == NULL ) + { + // first area in path, no cost + return 0.0f; + } + else + { + if ( !m_me->GetLocomotionInterface()->IsAreaTraversable( area ) ) + { + return -1.0f; + } + + // in training, avoid capturing the point until the human trainee does so + if ( TFGameRules()->IsInTraining() && + area->HasAttributeTF( TF_NAV_CONTROL_POINT ) && + !m_me->IsAnyPointBeingCaptured() && + !m_me->IsPlayerClass( TF_CLASS_ENGINEER ) ) // allow engineers to path so they can test travel distance for sentry placement + { + return -1.0f; + } + + // don't path through enemy spawn rooms + if ( ( m_me->GetTeamNumber() == TF_TEAM_RED && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) || + ( m_me->GetTeamNumber() == TF_TEAM_BLUE && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) ) ) + { + if ( !TFGameRules()->RoundHasBeenWon() ) + { + return -1.0f; + } + } + + // compute distance traveled along path so far + float dist; + + if ( ladder ) + { + dist = ladder->m_length; + } + else if ( length > 0.0 ) + { + dist = length; + } + else + { + dist = ( area->GetCenter() - fromArea->GetCenter() ).Length(); + } + + + // check height change + float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange( area ); + + if ( deltaZ >= m_stepHeight ) + { + if ( deltaZ >= m_maxJumpHeight ) + { + // too high to reach + return -1.0f; + } + + // jumping is slower than flat ground + const float jumpPenalty = 2.0f; + dist *= jumpPenalty; + } + else if ( deltaZ < -m_maxDropHeight ) + { + // too far to drop + return -1.0f; + } + + // add a random penalty unique to this character so they choose different routes to the same place + float preference = 1.0f; + + if ( m_routeType == DEFAULT_ROUTE && !m_me->IsMiniBoss() ) + { + // this term causes the same bot to choose different routes over time, + // but keep the same route for a period in case of repaths + int timeMod = (int)( gpGlobals->curtime / 10.0f ) + 1; + preference = 1.0f + 50.0f * ( 1.0f + FastCos( (float)( m_me->GetEntity()->entindex() * area->GetID() * timeMod ) ) ); + } + + if ( m_routeType == SAFEST_ROUTE ) + { + // avoid combat areas + if ( area->IsInCombat() ) + { + const float combatDangerCost = 4.0f; + dist *= combatDangerCost * area->GetCombatIntensity(); + } + + // if this area exposes us to enemy sentry fire, avoid it + const float sentryDangerCost = 5.0f; + if ( ( m_me->GetTeamNumber() == TF_TEAM_RED && area->HasAttributeTF( TF_NAV_BLUE_SENTRY_DANGER ) ) || + ( m_me->GetTeamNumber() == TF_TEAM_BLUE && area->HasAttributeTF( TF_NAV_RED_SENTRY_DANGER ) ) ) + { + dist *= sentryDangerCost; + } + } + + if ( m_me->IsPlayerClass( TF_CLASS_SPY ) ) + { + int enemyTeam = GetEnemyTeam( m_me->GetTeamNumber() ); + + // Since spies can get right up to enemy buildings, avoid them. + for ( int oit = 0; oit < IBaseObjectAutoList::AutoList().Count(); ++oit ) + { + CBaseObject *enemyObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[ oit ] ); + + if ( ( enemyObj->ObjectType() == OBJ_SENTRYGUN ) && + ( enemyObj->GetTeamNumber() == enemyTeam ) ) + { + enemyObj->UpdateLastKnownArea(); + + if ( enemyObj->GetLastKnownArea() == area ) + { + // There is an enemy building in this area - avoid it as a spy. + const float enemyBuildingCost = 10.0f; + dist *= enemyBuildingCost; + } + } + } + + // Spies avoid teammates, since they draw attention and gunfire. + const float teammateCost = 10.0f; + dist += dist * teammateCost * area->GetPlayerCount( m_me->GetTeamNumber() ); + + // We shouldn't be getting NaNs here. It will be handled when we return, but ideally + // it should be fixed here and not just worked around in NavAreaBuildPath. + DebuggerBreakOnNaN_StagingOnly( dist ); + } + + float cost = ( dist * preference ); + + if ( area->HasAttributes( NAV_MESH_FUNC_COST ) ) + { + cost *= area->ComputeFuncNavCost( m_me ); + DebuggerBreakOnNaN_StagingOnly( cost ); + } + + return cost + fromArea->GetCostSoFar(); + } + } + + CTFBot *m_me; + RouteType m_routeType; + float m_stepHeight; + float m_maxJumpHeight; + float m_maxDropHeight; +}; + + +//--------------------------------------------------------------------------------------------- +class CClosestTFPlayer +{ +public: + CClosestTFPlayer( const Vector &where, int team = TEAM_ANY ) + { + m_where = where; + m_closeRangeSq = FLT_MAX; + m_closePlayer = NULL; + m_team = team; + } + + CClosestTFPlayer( CBaseEntity *entity, int team = TEAM_ANY ) + { + m_where = entity->WorldSpaceCenter(); + m_closeRangeSq = FLT_MAX; + m_closePlayer = NULL; + m_team = team; + } + + bool operator() ( CBasePlayer *player ) + { + if ( !player->IsAlive() ) + return true; + + if ( player->GetTeamNumber() != TF_TEAM_RED && player->GetTeamNumber() != TF_TEAM_BLUE ) + return true; + + if ( m_team != TEAM_ANY && player->GetTeamNumber() != m_team ) + return true; + + CTFBot *bot = ToTFBot( player ); + if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) ) + return true; + + float rangeSq = ( m_where - player->GetAbsOrigin() ).LengthSqr(); + if ( rangeSq < m_closeRangeSq ) + { + m_closeRangeSq = rangeSq; + m_closePlayer = player; + } + return true; + } + + Vector m_where; + float m_closeRangeSq; + CBasePlayer *m_closePlayer; + int m_team; +}; + + +#endif // TF_BOT_H diff --git a/game/server/tf/bot/tf_bot_body.cpp b/game/server/tf/bot/tf_bot_body.cpp new file mode 100644 index 0000000..b1b87c5 --- /dev/null +++ b/game/server/tf/bot/tf_bot_body.cpp @@ -0,0 +1,42 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_body.cpp +// Team Fortress NextBot body interface +// Michael Booth, May 2010 + +#include "cbase.h" + +#include "tf_bot.h" +#include "tf_bot_body.h" + + +// +// Return how often we should sample our target's position and +// velocity to update our aim tracking, to allow realistic slop in tracking +// +float CTFBotBody::GetHeadAimTrackingInterval( void ) const +{ + CTFBot *me = (CTFBot *)GetBot(); + + // don't let Spies in MvM mode aim too precisely + if ( TFGameRules()->IsMannVsMachineMode() && me->IsPlayerClass( TF_CLASS_SPY ) ) + { + return 0.25f; + } + + switch( me->GetDifficulty() ) + { + case CTFBot::EXPERT: + return 0.05f; + + case CTFBot::HARD: + return 0.1f; + + case CTFBot::NORMAL: + return 0.25f; + + case CTFBot::EASY: + return 1.0f; + } + + return 0.0f; +} diff --git a/game/server/tf/bot/tf_bot_body.h b/game/server/tf/bot/tf_bot_body.h new file mode 100644 index 0000000..3c18d89 --- /dev/null +++ b/game/server/tf/bot/tf_bot_body.h @@ -0,0 +1,24 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_body.h +// Team Fortress NextBot body interface +// Michael Booth, May 2010 + +#ifndef TF_BOT_BODY_H +#define TF_BOT_BODY_H + +#include "NextBot/Player/NextBotPlayerBody.h" + +//---------------------------------------------------------------------------- +class CTFBotBody : public PlayerBody +{ +public: + CTFBotBody( INextBot *bot ) : PlayerBody( bot ) + { + } + + virtual ~CTFBotBody() { } + + virtual float GetHeadAimTrackingInterval( void ) const; // return how often we should sample our target's position and velocity to update our aim tracking, to allow realistic slop in tracking +}; + +#endif // TF_BOT_BODY_H
\ No newline at end of file diff --git a/game/server/tf/bot/tf_bot_locomotion.cpp b/game/server/tf/bot/tf_bot_locomotion.cpp new file mode 100644 index 0000000..8ff19c1 --- /dev/null +++ b/game/server/tf/bot/tf_bot_locomotion.cpp @@ -0,0 +1,137 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_locomotion.cpp +// Team Fortress NextBot locomotion interface +// Michael Booth, May 2010 + +#include "cbase.h" + +#include "tf_bot.h" +#include "tf_bot_locomotion.h" +#include "particle_parse.h" + + +//----------------------------------------------------------------------------------------- +void CTFBotLocomotion::Update( void ) +{ + BaseClass::Update(); + + CTFBot *me = ToTFBot( GetBot()->GetEntity() ); + if ( !me ) + { + return; + } + + // always 'crouch jump' + if ( IsOnGround() ) + { + if ( !me->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + // engineers need to crouch behind their guns + me->ReleaseCrouchButton(); + } + } + else + { + me->PressCrouchButton( 0.3f ); + } +} + + +//----------------------------------------------------------------------------------------- +// Move directly towards the given position +void CTFBotLocomotion::Approach( const Vector &pos, float goalWeight ) +{ + if ( TFGameRules()->IsMannVsMachineMode() ) + { + if ( !IsOnGround() && !IsClimbingOrJumping() ) + { + // no air control + return; + } + } + + BaseClass::Approach( pos, goalWeight ); +} + + +//----------------------------------------------------------------------------------------- +// Distance at which we will die if we fall +float CTFBotLocomotion::GetDeathDropHeight( void ) const +{ + return 1000.0f; +} + + +//----------------------------------------------------------------------------------------- +// Get maximum running speed +float CTFBotLocomotion::GetRunSpeed( void ) const +{ + CTFBot *me = (CTFBot *)GetBot()->GetEntity(); + return me->GetPlayerClass()->GetMaxSpeed(); +} + + +//----------------------------------------------------------------------------------------- +// Return true if given area can be used for navigation +bool CTFBotLocomotion::IsAreaTraversable( const CNavArea *baseArea ) const +{ + CTFBot *me = (CTFBot *)GetBot()->GetEntity(); + CTFNavArea *area = (CTFNavArea *)baseArea; + + if ( area->IsBlocked( me->GetTeamNumber() ) ) + { + return false; + } + + if ( !TFGameRules()->RoundHasBeenWon() || TFGameRules()->GetWinningTeam() != me->GetTeamNumber() ) + { + if ( area->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) && me->GetTeamNumber() == TF_TEAM_BLUE ) + { + return false; + } + + if ( area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) && me->GetTeamNumber() == TF_TEAM_RED ) + { + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------------------------- +bool CTFBotLocomotion::IsEntityTraversable( CBaseEntity *obstacle, TraverseWhenType when ) const +{ + // assume all players are "traversable" in that they will move or can be killed + if ( obstacle && obstacle->IsPlayer() ) + { + return true; + } + + return PlayerLocomotion::IsEntityTraversable( obstacle, when ); +} + + +void CTFBotLocomotion::Jump( void ) +{ + BaseClass::Jump(); + + CTFBot *me = ToTFBot( GetBot()->GetEntity() ); + if ( !me ) + { + return; + } + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + int iCustomJumpParticle = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( me, iCustomJumpParticle, bot_custom_jump_particle ); + if ( iCustomJumpParticle ) + { + const char *pEffectName = "rocketjump_smoke"; + DispatchParticleEffect( pEffectName, PATTACH_POINT_FOLLOW, me, "foot_L" ); + DispatchParticleEffect( pEffectName, PATTACH_POINT_FOLLOW, me, "foot_R" ); + } + } +} diff --git a/game/server/tf/bot/tf_bot_locomotion.h b/game/server/tf/bot/tf_bot_locomotion.h new file mode 100644 index 0000000..40cad56 --- /dev/null +++ b/game/server/tf/bot/tf_bot_locomotion.h @@ -0,0 +1,50 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_locomotion.h +// Team Fortress NextBot locomotion interface +// Michael Booth, May 2010 + +#ifndef TF_BOT_LOCOMOTION_H +#define TF_BOT_LOCOMOTION_H + +#include "NextBot/Player/NextBotPlayerLocomotion.h" + +//---------------------------------------------------------------------------- +class CTFBotLocomotion : public PlayerLocomotion +{ +public: + DECLARE_CLASS( CTFBotLocomotion, PlayerLocomotion ); + + CTFBotLocomotion( INextBot *bot ) : PlayerLocomotion( bot ) + { + } + + virtual ~CTFBotLocomotion() { } + + virtual void Update( void ); // (EXTEND) update internal state + + virtual void Approach( const Vector &pos, float goalWeight = 1.0f ); // move directly towards the given position + + virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump + virtual float GetDeathDropHeight( void ) const; // distance at which we will die if we fall + + virtual float GetRunSpeed( void ) const; // get maximum running speed + + virtual bool IsAreaTraversable( const CNavArea *baseArea ) const; // return true if given area can be used for navigation + virtual bool IsEntityTraversable( CBaseEntity *obstacle, TraverseWhenType when = EVENTUALLY ) const; + + // + // ILocomotion modifiers + // + virtual void Jump( void ) OVERRIDE; // initiate a simple undirected jump in the air + +protected: + virtual void AdjustPosture( const Vector &moveGoal ) { } // never crouch to navigate +}; + +inline float CTFBotLocomotion::GetMaxJumpHeight( void ) const +{ + // http://developer.valvesoftware.com/wiki/TF2/Team_Fortress_2_Mapper%27s_Reference + return 72.0f; +} + +#endif // TF_BOT_LOCOMOTION_H diff --git a/game/server/tf/bot/tf_bot_manager.cpp b/game/server/tf/bot/tf_bot_manager.cpp new file mode 100644 index 0000000..6ead184 --- /dev/null +++ b/game/server/tf/bot/tf_bot_manager.cpp @@ -0,0 +1,872 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +//---------------------------------------------------------------------------------------------------------------- +// tf_bot_manager.cpp +// Team Fortress NextBotManager +// Tom Bui, February 2010 +//---------------------------------------------------------------------------------------------------------------- + +#include "cbase.h" +#include "tf_bot_manager.h" + +#include "Player/NextBotPlayer.h" +#include "team.h" +#include "tf_bot.h" +#include "tf_gamerules.h" +#include "bot/map_entities/tf_bot_hint.h" +#include "bot/map_entities/tf_bot_hint_sentrygun.h" +#include "bot/map_entities/tf_bot_hint_teleporter_exit.h" + + +//---------------------------------------------------------------------------------------------------------------- + +// Creates and sets CTFBotManager as the NextBotManager singleton +static CTFBotManager sTFBotManager; + +extern ConVar tf_bot_force_class; +ConVar tf_bot_difficulty( "tf_bot_difficulty", "1", FCVAR_NONE, "Defines the skill of bots joining the game. Values are: 0=easy, 1=normal, 2=hard, 3=expert." ); +ConVar tf_bot_quota( "tf_bot_quota", "0", FCVAR_NONE, "Determines the total number of tf bots in the game." ); +ConVar tf_bot_quota_mode( "tf_bot_quota_mode", "normal", FCVAR_NONE, "Determines the type of quota.\nAllowed values: 'normal', 'fill', and 'match'.\nIf 'fill', the server will adjust bots to keep N players in the game, where N is bot_quota.\nIf 'match', the server will maintain a 1:N ratio of humans to bots, where N is bot_quota." ); +ConVar tf_bot_join_after_player( "tf_bot_join_after_player", "1", FCVAR_NONE, "If nonzero, bots wait until a player joins before entering the game." ); +ConVar tf_bot_auto_vacate( "tf_bot_auto_vacate", "1", FCVAR_NONE, "If nonzero, bots will automatically leave to make room for human players." ); +ConVar tf_bot_offline_practice( "tf_bot_offline_practice", "0", FCVAR_NONE, "Tells the server that it is in offline practice mode." ); +ConVar tf_bot_melee_only( "tf_bot_melee_only", "0", FCVAR_GAMEDLL, "If nonzero, TFBots will only use melee weapons" ); + +extern const char *GetRandomBotName( void ); +extern void CreateBotName( int iTeam, int iClassIndex, CTFBot::DifficultyType skill, char* pBuffer, int iBufferSize ); + +static bool UTIL_KickBotFromTeam( int kickTeam ) +{ + int i; + + // try to kick a dead bot first + for ( i = 1; i <= gpGlobals->maxClients; ++i ) + { + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + CTFBot* pBot = dynamic_cast<CTFBot*>(pPlayer); + + if (pBot == NULL) + continue; + + if ( pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) == false ) + continue; + + if ( ( pPlayer->GetFlags() & FL_FAKECLIENT ) == 0 ) + continue; + + if ( !pPlayer->IsAlive() && pPlayer->GetTeamNumber() == kickTeam ) + { + // its a bot on the right team - kick it + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", pPlayer->GetUserID() ) ); + + return true; + } + } + + // no dead bots, kick any bot on the given team + for ( i = 1; i <= gpGlobals->maxClients; ++i ) + { + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + CTFBot* pBot = dynamic_cast<CTFBot*>(pPlayer); + + if (pBot == NULL) + continue; + + if ( pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) == false ) + continue; + + if ( ( pPlayer->GetFlags() & FL_FAKECLIENT ) == 0 ) + continue; + + if (pPlayer->GetTeamNumber() == kickTeam) + { + // its a bot on the right team - kick it + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", pPlayer->GetUserID() ) ); + + return true; + } + } + + return false; +} + +//---------------------------------------------------------------------------------------------------------------- + +CTFBotManager::CTFBotManager() + : NextBotManager() + , m_flNextPeriodicThink( 0 ) +{ + NextBotManager::SetInstance( this ); +} + + +//---------------------------------------------------------------------------------------------------------------- +CTFBotManager::~CTFBotManager() +{ + NextBotManager::SetInstance( NULL ); +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::OnMapLoaded( void ) +{ + NextBotManager::OnMapLoaded(); + + ClearStuckBotData(); +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::OnRoundRestart( void ) +{ + NextBotManager::OnRoundRestart(); + + // clear all hint ownership + CTFBotHint *hint = NULL; + while( ( hint = (CTFBotHint *)( gEntList.FindEntityByClassname( hint, "func_tfbot_hint" ) ) ) != NULL ) + { + hint->SetOwnerEntity( NULL ); + } + + CTFBotHintSentrygun *sentryHint = NULL; + while( ( sentryHint = (CTFBotHintSentrygun *)( gEntList.FindEntityByClassname( sentryHint, "bot_hint_sentrygun" ) ) ) != NULL ) + { + sentryHint->SetOwnerEntity( NULL ); + } + + CTFBotHintTeleporterExit *teleporterHint = NULL; + while( ( teleporterHint = (CTFBotHintTeleporterExit *)( gEntList.FindEntityByClassname( teleporterHint, "bot_hint_teleporter_exit" ) ) ) != NULL ) + { + teleporterHint->SetOwnerEntity( NULL ); + } + + +#ifdef TF_CREEP_MODE + m_creepExperience[ TF_TEAM_RED ] = 0; + m_creepExperience[ TF_TEAM_BLUE ] = 0; +#endif + + m_isMedeivalBossScenarioSetup = false; +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::Update() +{ + MaintainBotQuota(); + + DrawStuckBotData(); + +#ifdef TF_CREEP_MODE + UpdateCreepWaves(); +#endif + + NextBotManager::Update(); +} + + +#ifdef TF_CREEP_MODE +ConVar tf_creep_initial_delay( "tf_creep_initial_delay", "30" ); +ConVar tf_creep_wave_interval( "tf_creep_wave_interval", "30" ); +ConVar tf_creep_wave_count( "tf_creep_wave_count", "3" ); +ConVar tf_creep_class( "tf_creep_class", "heavyweapons" ); +ConVar tf_creep_level_up( "tf_creep_level_up", "6" ); + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::UpdateCreepWaves() +{ + if ( !TFGameRules()->IsCreepWaveMode() ) + return; + + if ( TFGameRules()->RoundHasBeenWon() ) + { + // no more creep waves - game is over + return; + } + + if ( TFGameRules()->InSetup() || TFGameRules()->State_Get() == GR_STATE_STARTGAME || TFGameRules()->State_Get() == GR_STATE_PREROUND ) + { + // no creeps at start of round + m_creepWaveTimer.Start( tf_creep_initial_delay.GetFloat() ); + + // delete all creeps + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast< CBasePlayer * >( UTIL_PlayerByIndex( i ) ); + + if ( !player ) + continue; + + if ( FNullEnt( player->edict() ) ) + continue; + + CTFBot *creep = ToTFBot( player ); + if ( !creep || !creep->HasAttribute( CTFBot::IS_NPC ) ) + continue; + + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", player->GetUserID() ) ); + } + + return; + } + + if ( m_creepWaveTimer.IsElapsed() ) + { + m_creepWaveTimer.Start( tf_creep_wave_interval.GetFloat() ); + + SpawnCreepWave( TF_TEAM_RED ); + SpawnCreepWave( TF_TEAM_BLUE ); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::SpawnCreepWave( int team ) +{ + CTFBotSquad *squad = new CTFBotSquad; + + for( int i=0; i<tf_creep_wave_count.GetInt(); ++i ) + { + SpawnCreep( team, squad ); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::SpawnCreep( int team, CTFBotSquad *squad ) +{ + CTFBot *bot = NextBotCreatePlayerBot< CTFBot >( "Creep" ); + + if ( !bot ) + return; + + bot->SetAttribute( CTFBot::IS_NPC ); + bot->HandleCommand_JoinTeam( team == TF_TEAM_RED ? "red" : "blue" ); + bot->SetDifficulty( CTFBot::NORMAL ); + bot->HandleCommand_JoinClass( tf_creep_class.GetString() ); + bot->JoinSquad( squad ); + bot->AddGlowEffect(); + //BotGenerateAndWearItem( bot, "Honest Halo" ); +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::OnCreepKilled( CTFPlayer *killer ) +{ + CTFBot *bot = ToTFBot( killer ); + if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) ) + return; + + ++m_creepExperience[ killer->GetTeamNumber() ]; + +/* + int xp = m_creepExperience[ killer->GetTeamNumber() ]; + int level = xp / tf_creep_level_up.GetInt(); + int left = xp % tf_creep_level_up.GetInt(); + + char text[256]; + Q_snprintf( text, sizeof(text), "%s killed a creep. %s team LVL = %d+%d/%d\n", + killer->GetPlayerName(), + killer->GetTeamNumber() == TF_TEAM_RED ? "Red" : "Blue", + level+1, left, tf_creep_level_up.GetInt() ); + + UTIL_ClientPrintAll( HUD_PRINTTALK, text ); +*/ + + UTIL_ClientPrintAll( HUD_PRINTTALK, "%s killed a creep" ); +} + +#endif // TF_CREEP_MODE + +//---------------------------------------------------------------------------------------------------------------- +bool CTFBotManager::RemoveBotFromTeamAndKick( int nTeam ) +{ + CUtlVector< CTFPlayer* > vecCandidates; + + // Gather potential candidates + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + + if ( pPlayer == NULL ) + continue; + + if ( FNullEnt( pPlayer->edict() ) ) + continue; + + if ( !pPlayer->IsConnected() ) + continue; + + CTFBot* pBot = dynamic_cast<CTFBot*>( pPlayer ); + if ( pBot && pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) ) + { + if ( pBot->GetTeamNumber() == nTeam ) + { + vecCandidates.AddToTail( pPlayer ); + } + } + } + + CTFPlayer *pVictim = NULL; + if ( vecCandidates.Count() > 0 ) + { + // first look for bots that are currently dead + FOR_EACH_VEC( vecCandidates, i ) + { + CTFPlayer *pPlayer = vecCandidates[i]; + if ( pPlayer && !pPlayer->IsAlive() ) + { + pVictim = pPlayer; + break; + } + } + + // if we didn't fine one, try to kick anyone on the team + if ( !pVictim ) + { + FOR_EACH_VEC( vecCandidates, i ) + { + CTFPlayer *pPlayer = vecCandidates[i]; + if ( pPlayer ) + { + pVictim = pPlayer; + break; + } + } + } + } + + if ( pVictim ) + { + if ( pVictim->IsAlive() ) + { + pVictim->CommitSuicide(); + } + pVictim->ForceChangeTeam( TEAM_UNASSIGNED ); // skipping TEAM_SPECTATOR because some servers don't allow spectators + UTIL_KickBotFromTeam( TEAM_UNASSIGNED ); + return true; + } + + return false; +} + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::MaintainBotQuota() +{ + if ( TheNavMesh->IsGenerating() ) + return; + + if ( g_fGameOver ) + return; + + // new players can't spawn immediately after the round has been going for some time + if ( !TFGameRules() ) + return; + + // training mode controls the bots + if ( TFGameRules()->IsInTraining() ) + return; + + // if it is not time to do anything... + if ( gpGlobals->curtime < m_flNextPeriodicThink ) + return; + + // think every quarter second + m_flNextPeriodicThink = gpGlobals->curtime + 0.25f; + + // don't add bots until local player has been registered, to make sure he's player ID #1 + if ( !engine->IsDedicatedServer() ) + { + CBasePlayer *pPlayer = UTIL_GetListenServerHost(); + if ( !pPlayer ) + return; + } + + // We want to balance based on who's playing on game teams not necessary who's on team spectator, etc. + int nConnectedClients = 0; + int nTFBots = 0; + int nTFBotsOnGameTeams = 0; + int nNonTFBotsOnGameTeams = 0; + int nSpectators = 0; + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + + if ( pPlayer == NULL ) + continue; + + if ( FNullEnt( pPlayer->edict() ) ) + continue; + + if ( !pPlayer->IsConnected() ) + continue; + + CTFBot* pBot = dynamic_cast<CTFBot*>( pPlayer ); + if ( pBot && pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) ) + { + nTFBots++; + if ( pPlayer->GetTeamNumber() == TF_TEAM_RED || pPlayer->GetTeamNumber() == TF_TEAM_BLUE ) + { + nTFBotsOnGameTeams++; + } + } + else + { + if ( pPlayer->GetTeamNumber() == TF_TEAM_RED || pPlayer->GetTeamNumber() == TF_TEAM_BLUE ) + { + nNonTFBotsOnGameTeams++; + } + else if ( pPlayer->GetTeamNumber() == TEAM_SPECTATOR ) + { + nSpectators++; + } + } + + nConnectedClients++; + } + + int desiredBotCount = tf_bot_quota.GetInt(); + int nTotalNonTFBots = nConnectedClients - nTFBots; + + if ( FStrEq( tf_bot_quota_mode.GetString(), "fill" ) ) + { + desiredBotCount = MAX( 0, desiredBotCount - nNonTFBotsOnGameTeams ); + } + else if ( FStrEq( tf_bot_quota_mode.GetString(), "match" ) ) + { + // If bot_quota_mode is 'match', we want the number of bots to be bot_quota * total humans + desiredBotCount = (int)MAX( 0, tf_bot_quota.GetFloat() * nNonTFBotsOnGameTeams ); + } + + // wait for a player to join, if necessary + if ( tf_bot_join_after_player.GetBool() ) + { + if ( ( nNonTFBotsOnGameTeams == 0 ) && ( nSpectators == 0 ) ) + { + desiredBotCount = 0; + } + } + + // if bots will auto-vacate, we need to keep one slot open to allow players to join + if ( tf_bot_auto_vacate.GetBool() ) + { + desiredBotCount = MIN( desiredBotCount, gpGlobals->maxClients - nTotalNonTFBots - 1 ); + } + else + { + desiredBotCount = MIN( desiredBotCount, gpGlobals->maxClients - nTotalNonTFBots ); + } + + // add bots if necessary + if ( desiredBotCount > nTFBotsOnGameTeams ) + { + // don't try to add a bot if it would unbalance + if ( !TFGameRules()->WouldChangeUnbalanceTeams( TF_TEAM_BLUE, TEAM_UNASSIGNED ) || + !TFGameRules()->WouldChangeUnbalanceTeams( TF_TEAM_RED, TEAM_UNASSIGNED ) ) + { + CTFBot *pBot = GetAvailableBotFromPool(); + if ( pBot == NULL ) + { + pBot = NextBotCreatePlayerBot< CTFBot >( GetRandomBotName() ); + } + if ( pBot ) + { + pBot->SetAttribute( CTFBot::QUOTA_MANANGED ); + + // join a team before we pick our class, since we use our teammates to decide what class to be + pBot->HandleCommand_JoinTeam( "auto" ); + + const char *classname = FStrEq( tf_bot_force_class.GetString(), "" ) ? pBot->GetNextSpawnClassname() : tf_bot_force_class.GetString(); + pBot->HandleCommand_JoinClass( classname ); + + // give the bot a proper name + char name[256]; + CTFBot::DifficultyType skill = pBot->GetDifficulty(); + CreateBotName( pBot->GetTeamNumber(), pBot->GetPlayerClass()->GetClassIndex(), skill, name, sizeof( name ) ); + engine->SetFakeClientConVarValue( pBot->edict(), "name", name ); + + // Keep track of any bots we add during a match + CMatchInfo *pMatchInfo = GTFGCClientSystem()->GetMatch(); + if ( pMatchInfo ) + { + pMatchInfo->m_nBotsAdded++; + } + } + } + } + else if ( desiredBotCount < nTFBotsOnGameTeams ) + { + // kick a bot to maintain quota + + // first remove any unassigned bots + if ( UTIL_KickBotFromTeam( TEAM_UNASSIGNED ) ) + return; + + int kickTeam; + + CTeam *pRed = GetGlobalTeam( TF_TEAM_RED ); + CTeam *pBlue = GetGlobalTeam( TF_TEAM_BLUE ); + + // remove from the team that has more players + if ( pBlue->GetNumPlayers() > pRed->GetNumPlayers() ) + { + kickTeam = TF_TEAM_BLUE; + } + else if ( pBlue->GetNumPlayers() < pRed->GetNumPlayers() ) + { + kickTeam = TF_TEAM_RED; + } + // remove from the team that's winning + else if ( pBlue->GetScore() > pRed->GetScore() ) + { + kickTeam = TF_TEAM_BLUE; + } + else if ( pBlue->GetScore() < pRed->GetScore() ) + { + kickTeam = TF_TEAM_RED; + } + else + { + // teams and scores are equal, pick a team at random + kickTeam = (RandomInt( 0, 1 ) == 0) ? TF_TEAM_BLUE : TF_TEAM_RED; + } + + // attempt to kick a bot from the given team + if ( UTIL_KickBotFromTeam( kickTeam ) ) + return; + + // if there were no bots on the team, kick a bot from the other team + UTIL_KickBotFromTeam( kickTeam == TF_TEAM_BLUE ? TF_TEAM_RED : TF_TEAM_BLUE ); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +bool CTFBotManager::IsAllBotTeam( int iTeam ) +{ + CTeam *pTeam = GetGlobalTeam( iTeam ); + if ( pTeam == NULL ) + { + return false; + } + + // check to see if any players on the team are humans + for ( int i = 0, n = pTeam->GetNumPlayers(); i < n; ++i ) + { + CTFPlayer *pPlayer = ToTFPlayer( pTeam->GetPlayer( i ) ); + if ( pPlayer == NULL ) + { + continue; + } + if ( pPlayer->IsBot() == false ) + { + return false; + } + } + + // if we made it this far, then they must all be bots! + if ( pTeam->GetNumPlayers() != 0 ) + { + return true; + } + + // okay, this is a bit trickier... + // if there are no people on this team, then we need to check the "assigned" human team + return TFGameRules()->GetAssignedHumanTeam() != iTeam; +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::SetIsInOfflinePractice(bool bIsInOfflinePractice) +{ + tf_bot_offline_practice.SetValue( bIsInOfflinePractice ? 1 : 0 ); +} + + +//---------------------------------------------------------------------------------------------------------------- +bool CTFBotManager::IsInOfflinePractice() const +{ + return tf_bot_offline_practice.GetInt() != 0; +} + + +//---------------------------------------------------------------------------------------------------------------- +bool CTFBotManager::IsMeleeOnly() const +{ + return tf_bot_melee_only.GetBool(); +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::RevertOfflinePracticeConvars() +{ + tf_bot_quota.Revert(); + tf_bot_quota_mode.Revert(); + tf_bot_auto_vacate.Revert(); + tf_bot_difficulty.Revert(); + tf_bot_offline_practice.Revert(); +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::LevelShutdown() +{ + m_flNextPeriodicThink = 0.0f; + if ( IsInOfflinePractice() ) + { + RevertOfflinePracticeConvars(); + SetIsInOfflinePractice( false ); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +CTFBot* CTFBotManager::GetAvailableBotFromPool() +{ + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + CTFBot* pBot = dynamic_cast<CTFBot*>(pPlayer); + + if (pBot == NULL) + continue; + + if ( ( pBot->GetFlags() & FL_FAKECLIENT ) == 0 ) + continue; + + if ( pBot->GetTeamNumber() == TEAM_SPECTATOR || pBot->GetTeamNumber() == TEAM_UNASSIGNED ) + { + pBot->ClearAttribute( CTFBot::QUOTA_MANANGED ); + return pBot; + } + } + return NULL; +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::OnForceAddedBots( int iNumAdded ) +{ + tf_bot_quota.SetValue( tf_bot_quota.GetInt() + iNumAdded ); + m_flNextPeriodicThink = gpGlobals->curtime + 1.0f; +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::OnForceKickedBots( int iNumKicked ) +{ + tf_bot_quota.SetValue( MAX( tf_bot_quota.GetInt() - iNumKicked, 0 ) ); + // allow time for the bots to be kicked + m_flNextPeriodicThink = gpGlobals->curtime + 2.0f; +} + + +//---------------------------------------------------------------------------------------------------------------- +CTFBotManager &TheTFBots( void ) +{ + return static_cast<CTFBotManager&>( TheNextBots() ); +} + + + +//---------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( tf_bot_debug_stuck_log, "Given a server logfile, visually display bot stuck locations.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + // Listenserver host or rcon access only! + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( args.ArgC() < 2 ) + { + DevMsg( "%s <logfilename>\n", args.Arg(0) ); + return; + } + + FileHandle_t file = filesystem->Open( args.Arg(1), "r", "GAME" ); + + const int maxBufferSize = 1024; + char buffer[ maxBufferSize ]; + + char logMapName[ maxBufferSize ]; + logMapName[0] = '\000'; + + TheTFBots().ClearStuckBotData(); + + if ( file ) + { + int line = 0; + while( !filesystem->EndOfFile( file ) ) + { + filesystem->ReadLine( buffer, maxBufferSize, file ); + ++line; + + strtok( buffer, ":" ); + strtok( NULL, ":" ); + strtok( NULL, ":" ); + char *first = strtok( NULL, " " ); + + if ( !first ) + continue; + + if ( !strcmp( first, "Loading" ) ) + { + // L 08/08/2012 - 15:10:47: Loading map "mvm_coaltown" + strtok( NULL, " " ); + char *mapname = strtok( NULL, "\"" ); + + if ( mapname ) + { + strcpy( logMapName, mapname ); + Warning( "*** Log file from map '%s'\n", mapname ); + } + } + else if ( first[0] == '\"' ) + { + // might be a player ID + + char *playerClassname = &first[1]; + + char *nameEnd = playerClassname; + while( *nameEnd != '\000' && *nameEnd != '<' ) + ++nameEnd; + *nameEnd = '\000'; + + char *botIDString = ++nameEnd; + char *IDEnd = botIDString; + while( *IDEnd != '\000' && *IDEnd != '>' ) + ++IDEnd; + *IDEnd = '\000'; + + int botID = atoi( botIDString ); + + char *second = strtok( NULL, " " ); + if ( second && !strcmp( second, "stuck" ) ) + { + CStuckBot *stuckBot = TheTFBots().FindOrCreateStuckBot( botID, playerClassname ); + + CStuckBotEvent *stuckEvent = new CStuckBotEvent; + + + // L 08/08/2012 - 15:15:05: "Scout<53><BOT><Blue>" stuck (position "-180.61 2471.29 216.04") (duration "2.52") L 08/08/2012 - 15:15:05: path_goal ( "-180.61 2471.29 216.04" ) + strtok( NULL, " (\"" ); // (position + + stuckEvent->m_stuckSpot.x = (float)atof( strtok( NULL, " )\"" ) ); + stuckEvent->m_stuckSpot.y = (float)atof( strtok( NULL, " )\"" ) ); + stuckEvent->m_stuckSpot.z = (float)atof( strtok( NULL, " )\"" ) ); + + strtok( NULL, ") (\"" ); + stuckEvent->m_stuckDuration = (float)atof( strtok( NULL, "\"" ) ); + + strtok( NULL, ") (\"-L0123456789/:" ); // path_goal + + char *goal = strtok( NULL, ") (\"" ); + + if ( goal && strcmp( goal, "NULL" ) ) + { + stuckEvent->m_isGoalValid = true; + + stuckEvent->m_goalSpot.x = (float)atof( goal ); + stuckEvent->m_goalSpot.y = (float)atof( strtok( NULL, ") (\"" ) ); + stuckEvent->m_goalSpot.z = (float)atof( strtok( NULL, ") (\"" ) ); + } + else + { + stuckEvent->m_isGoalValid = false; + } + + stuckBot->m_stuckEventVector.AddToTail( stuckEvent ); + } + } + } + + filesystem->Close( file ); + } + else + { + Warning( "Can't open file '%s'\n", args.Arg(1) ); + } + + //TheTFBots().DrawStuckBotData(); +} + + +//---------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( tf_bot_debug_stuck_log_clear, "Clear currently loaded bot stuck data", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + // Listenserver host or rcon access only! + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheTFBots().ClearStuckBotData(); +} + + +//---------------------------------------------------------------------------------------------------------------- +// for parsing and debugging stuck bot server logs +void CTFBotManager::ClearStuckBotData() +{ + m_stuckBotVector.PurgeAndDeleteElements(); +} + + +//---------------------------------------------------------------------------------------------------------------- +// for parsing and debugging stuck bot server logs +CStuckBot *CTFBotManager::FindOrCreateStuckBot( int id, const char *playerClass ) +{ + for( int i=0; i<m_stuckBotVector.Count(); ++i ) + { + CStuckBot *stuckBot = m_stuckBotVector[i]; + + if ( stuckBot->IsMatch( id, playerClass ) ) + { + return stuckBot; + } + } + + // new instance of a stuck bot + CStuckBot *newStuckBot = new CStuckBot( id, playerClass ); + m_stuckBotVector.AddToHead( newStuckBot ); + + return newStuckBot; +} + + +//---------------------------------------------------------------------------------------------------------------- +void CTFBotManager::DrawStuckBotData( float deltaT ) +{ + if ( engine->IsDedicatedServer() ) + return; + + if ( !m_stuckDisplayTimer.IsElapsed() ) + return; + + m_stuckDisplayTimer.Start( deltaT ); + + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( player == NULL ) + return; + +// Vector forward; +// AngleVectors( player->EyeAngles(), &forward ); + + for( int i=0; i<m_stuckBotVector.Count(); ++i ) + { + for( int j=0; j<m_stuckBotVector[i]->m_stuckEventVector.Count(); ++j ) + { + m_stuckBotVector[i]->m_stuckEventVector[j]->Draw( deltaT ); + } + + for( int j=0; j<m_stuckBotVector[i]->m_stuckEventVector.Count()-1; ++j ) + { + NDebugOverlay::HorzArrow( m_stuckBotVector[i]->m_stuckEventVector[j]->m_stuckSpot, + m_stuckBotVector[i]->m_stuckEventVector[j+1]->m_stuckSpot, + 3, 100, 0, 255, 255, true, deltaT ); + } + + NDebugOverlay::Text( m_stuckBotVector[i]->m_stuckEventVector[0]->m_stuckSpot, CFmtStr( "%s(#%d)", m_stuckBotVector[i]->m_name, m_stuckBotVector[i]->m_id ), false, deltaT ); + } +} + + diff --git a/game/server/tf/bot/tf_bot_manager.h b/game/server/tf/bot/tf_bot_manager.h new file mode 100644 index 0000000..4f5ec5f --- /dev/null +++ b/game/server/tf/bot/tf_bot_manager.h @@ -0,0 +1,147 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_manager.h +// Team Fortress NextBotManager +// Tom Bui, May 2010 + +#ifndef TF_BOT_MANAGER_H +#define TF_BOT_MANAGER_H + +#include "NextBotManager.h" +#include "tf_team.h" + +class CTFBot; +class CTFPlayer; +class CTFBotSquad; +class CStuckBotEvent; + + + +//---------------------------------------------------------------------------------------------- +// For parsing and displaying stuck events from server logs. +class CStuckBot +{ +public: + CStuckBot( int id, const char *name ) + { + m_id = id; + Q_strncpy( m_name, name, 256 ); + } + + bool IsMatch( int id, const char *name ) + { + return ( id == m_id && FStrEq( name, m_name ) ); + } + + char m_name[256]; + int m_id; + + CUtlVector< CStuckBotEvent * > m_stuckEventVector; +}; + + + +//---------------------------------------------------------------------------------------------- +// For parsing and displaying stuck events from server logs. +class CStuckBotEvent +{ +public: + Vector m_stuckSpot; + float m_stuckDuration; + Vector m_goalSpot; + bool m_isGoalValid; + + void Draw( float deltaT = 0.1f ) + { + NDebugOverlay::Cross3D( m_stuckSpot, 5.0f, 255, 255, 0, true, deltaT ); + + if ( m_isGoalValid ) + { + if ( m_stuckDuration > 6.0f ) + { + NDebugOverlay::HorzArrow( m_stuckSpot, m_goalSpot, 2.0f, 255, 0, 0, 255, true, deltaT ); + } + else if ( m_stuckDuration > 3.0f ) + { + NDebugOverlay::HorzArrow( m_stuckSpot, m_goalSpot, 2.0f, 255, 255, 0, 255, true, deltaT ); + } + else + { + NDebugOverlay::HorzArrow( m_stuckSpot, m_goalSpot, 2.0f, 0, 255, 0, 255, true, deltaT ); + } + } + } +}; + + +//---------------------------------------------------------------------------------------------- +class CTFBotManager : public NextBotManager +{ +public: + CTFBotManager(); + virtual ~CTFBotManager(); + + virtual void Update(); + void LevelShutdown(); + + virtual void OnMapLoaded( void ); // when the server has changed maps + virtual void OnRoundRestart( void ); // when the scenario restarts + + bool IsAllBotTeam( int iTeam ); + bool IsInOfflinePractice() const; + bool IsMeleeOnly() const; + + CTFBot* GetAvailableBotFromPool(); + + void OnForceAddedBots( int iNumAdded ); + void OnForceKickedBots( int iNumKicked ); + + void ClearStuckBotData(); + CStuckBot *FindOrCreateStuckBot( int id, const char *playerClass ); // for parsing and debugging stuck bot server logs + void DrawStuckBotData( float deltaT = 0.1f ); + +#ifdef TF_CREEP_MODE + void OnCreepKilled( CTFPlayer *killer ); +#endif + + bool RemoveBotFromTeamAndKick( int nTeam ); + +protected: + void MaintainBotQuota(); + void SetIsInOfflinePractice( bool bIsInOfflinePractice ); + void RevertOfflinePracticeConvars(); + + float m_flNextPeriodicThink; + +#ifdef TF_CREEP_MODE + void UpdateCreepWaves(); + CountdownTimer m_creepWaveTimer; + + void SpawnCreep( int team, CTFBotSquad *squad ); + void SpawnCreepWave( int team ); + + int m_creepExperience[ TF_TEAM_COUNT ]; +#endif + + void UpdateMedievalBossScenario(); + bool m_isMedeivalBossScenarioSetup; + void SetupMedievalBossScenario(); + + CUtlVector< CBaseEntity * > m_archerSpawnVector; + + struct ArcherAssignmentInfo + { + CHandle< CBaseCombatCharacter > m_archer; + CHandle< CBaseEntity > m_mark; + }; + CUtlVector< ArcherAssignmentInfo > m_archerMarkVector; + + CountdownTimer m_archerTimer; + + CUtlVector< CStuckBot * > m_stuckBotVector; + CountdownTimer m_stuckDisplayTimer; +}; + +// singleton accessor +CTFBotManager &TheTFBots( void ); + +#endif // TF_BOT_MANAGER_H diff --git a/game/server/tf/bot/tf_bot_squad.cpp b/game/server/tf/bot/tf_bot_squad.cpp new file mode 100644 index 0000000..91cc914 --- /dev/null +++ b/game/server/tf/bot/tf_bot_squad.cpp @@ -0,0 +1,294 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_squad.h +// Small groups of TFBot, managed as a unit +// Michael Booth, November 2009 + +#include "cbase.h" +#include "tf_bot.h" +#include "tf_bot_squad.h" + + +//---------------------------------------------------------------------- +CTFBotSquad::CTFBotSquad( void ) +{ + m_leader = NULL; + m_formationSize = -1.0f; + m_bShouldPreserveSquad = false; +} + + +//---------------------------------------------------------------------- +void CTFBotSquad::Join( CTFBot *bot ) +{ + // first member is the leader + if ( m_roster.Count() == 0 ) + { + m_leader = bot; + } + else if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + bot->SetFlagTarget( NULL ); + } + + m_roster.AddToTail( bot ); +} + + +//---------------------------------------------------------------------- +void CTFBotSquad::Leave( CTFBot *bot ) +{ + m_roster.FindAndRemove( bot ); + + if ( bot == m_leader.Get() ) + { + m_leader = NULL; + + // pick the next living leader that's left in the squad + if ( m_bShouldPreserveSquad ) + { + CUtlVector< CTFBot* > members; + CollectMembers( &members ); + if ( members.Count() ) + { + m_leader = members[0]; + } + } + } + else if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + AssertMsg( !bot->HasFlagTaget(), "Squad member shouldn't have a flag target. Always follow the leader." ); + CCaptureFlag *pFlag = bot->GetFlagToFetch(); + if ( pFlag ) + { + bot->SetFlagTarget( pFlag ); + } + } + + if ( GetMemberCount() == 0 ) + { + DisbandAndDeleteSquad(); + } +} + + +//---------------------------------------------------------------------- +INextBotEventResponder *CTFBotSquad::FirstContainedResponder( void ) const +{ + return m_roster.Count() ? m_roster[0] : NULL; +} + + +//---------------------------------------------------------------------- +INextBotEventResponder *CTFBotSquad::NextContainedResponder( INextBotEventResponder *current ) const +{ + CTFBot *currentBot = (CTFBot *)current; + + int i = m_roster.Find( currentBot ); + + if ( i == m_roster.InvalidIndex() ) + return NULL; + + if ( ++i >= m_roster.Count() ) + return NULL; + + return (CTFBot *)m_roster[i]; +} + + +//---------------------------------------------------------------------- +CTFBot *CTFBotSquad::GetLeader( void ) const +{ + return m_leader; +} + + +//---------------------------------------------------------------------- +void CTFBotSquad::CollectMembers( CUtlVector< CTFBot * > *memberVector ) const +{ + for( int i=0; i<m_roster.Count(); ++i ) + { + if ( m_roster[i] != NULL && m_roster[i]->IsAlive() ) + { + memberVector->AddToTail( m_roster[i] ); + } + } +} + + +//---------------------------------------------------------------------- +CTFBotSquad::Iterator CTFBotSquad::GetFirstMember( void ) const +{ + // find first non-NULL member + for( int i=0; i<m_roster.Count(); ++i ) + if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() ) + return Iterator( m_roster[i], i ); + + return InvalidIterator(); +} + + +//---------------------------------------------------------------------- +CTFBotSquad::Iterator CTFBotSquad::GetNextMember( const Iterator &it ) const +{ + // find next non-NULL member + for( int i=it.m_index+1; i<m_roster.Count(); ++i ) + if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() ) + return Iterator( m_roster[i], i ); + + return InvalidIterator(); +} + + +//---------------------------------------------------------------------- +int CTFBotSquad::GetMemberCount( void ) const +{ + // count the non-NULL members + int count = 0; + for( int i=0; i<m_roster.Count(); ++i ) + if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() ) + ++count; + + return count; +} + + +//---------------------------------------------------------------------- +// Return the speed of the slowest member of the squad +float CTFBotSquad::GetSlowestMemberSpeed( bool includeLeader ) const +{ + float speed = FLT_MAX; + + int i = includeLeader ? 0 : 1; + + for( ; i<m_roster.Count(); ++i ) + { + if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() ) + { + float memberSpeed = m_roster[i]->MaxSpeed(); + if ( memberSpeed < speed ) + { + speed = memberSpeed; + } + } + } + + return speed; +} + + +//---------------------------------------------------------------------- +// Return the speed of the slowest member of the squad, +// considering their ideal class speed. +float CTFBotSquad::GetSlowestMemberIdealSpeed( bool includeLeader ) const +{ + float speed = FLT_MAX; + + int i = includeLeader ? 0 : 1; + + for( ; i<m_roster.Count(); ++i ) + { + if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() ) + { + float memberSpeed = m_roster[i]->GetPlayerClass()->GetMaxSpeed(); + if ( memberSpeed < speed ) + { + speed = memberSpeed; + } + } + } + + return speed; +} + + +//---------------------------------------------------------------------- +// Return the maximum formation error of the squad's memebers. +float CTFBotSquad::GetMaxSquadFormationError( void ) const +{ + float maxError = 0.0f; + + // skip the leader since he's what the formation forms around + for( int i=1; i<m_roster.Count(); ++i ) + { + if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() ) + { + float error = m_roster[i]->GetSquadFormationError(); + if ( error > maxError ) + { + maxError = error; + } + } + } + + return maxError; +} + + +//---------------------------------------------------------------------- +// Return true if the squad leader needs to wait for members to catch up, ignoring those who have broken ranks +bool CTFBotSquad::ShouldSquadLeaderWaitForFormation( void ) const +{ + // skip the leader since he's what the formation forms around + for( int i=1; i<m_roster.Count(); ++i ) + { + // the squad leader should wait if any member is out of position, but not yet broken ranks + if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() ) + { + if ( m_roster[i]->GetSquadFormationError() >= 1.0f && + !m_roster[i]->HasBrokenFormation() && + !m_roster[i]->GetLocomotionInterface()->IsStuck() && + !m_roster[i]->IsPlayerClass( TF_CLASS_MEDIC ) ) // Medics do their own thing + { + // wait for me! + return true; + } + } + } + + return false; +} + + +//---------------------------------------------------------------------- +// Return true if the squad is in formation (everyone is in or nearly in their desired positions) +bool CTFBotSquad::IsInFormation( void ) const +{ + // skip the leader since he's what the formation forms around + for( int i=1; i<m_roster.Count(); ++i ) + { + if ( m_roster[i].Get() != NULL && m_roster[i]->IsAlive() ) + { + if ( m_roster[i]->HasBrokenFormation() || + m_roster[i]->GetLocomotionInterface()->IsStuck() || + m_roster[i]->IsPlayerClass( TF_CLASS_MEDIC ) ) // Medics do their own thing + { + // I'm not "in formation" + continue; + } + + if ( m_roster[i]->GetSquadFormationError() > 0.75f ) + { + // I'm not in position yet + return false; + } + } + } + + return true; +} + +//---------------------------------------------------------------------- +// Tell all members to leave the squad and then delete itself +void CTFBotSquad::DisbandAndDeleteSquad( void ) +{ + // Tell each member of the squad to remove this reference + for( int i=0; i < m_roster.Count(); ++i ) + { + if ( m_roster[i].Get() != NULL ) + { + m_roster[i]->DeleteSquad(); + } + } + + delete this; +}
\ No newline at end of file diff --git a/game/server/tf/bot/tf_bot_squad.h b/game/server/tf/bot/tf_bot_squad.h new file mode 100644 index 0000000..c3b145f --- /dev/null +++ b/game/server/tf/bot/tf_bot_squad.h @@ -0,0 +1,121 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_squad.h +// Small groups of TFBot, managed as a unit +// Michael Booth, November 2009 + +#ifndef TF_BOT_SQUAD_H +#define TF_BOT_SQUAD_H + +#include "NextBot/NextBotEventResponderInterface.h" + +class CTFBot; + +class CTFBotSquad : public INextBotEventResponder +{ +public: + CTFBotSquad( void ); + virtual ~CTFBotSquad() { } + + // EventResponder ------ + virtual INextBotEventResponder *FirstContainedResponder( void ) const; + virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const; + //---------------------- + + bool IsMember( CTFBot *bot ) const; // is the given bot in this squad? + bool IsLeader( CTFBot *bot ) const; // is the given bot the leader of this squad? + +// CTFBot *GetMember( int i ); + int GetMemberCount( void ) const; + + CTFBot *GetLeader( void ) const; + + class Iterator + { + public: + Iterator( void ) + { + m_bot = NULL; + m_index = -1; + } + + Iterator( CTFBot *bot, int index ) + { + m_bot = bot; + m_index = index; + } + + CTFBot *operator() ( void ) + { + return m_bot; + } + + bool operator==( const Iterator &it ) const { return m_bot == it.m_bot && m_index == it.m_index; } + bool operator!=( const Iterator &it ) const { return m_bot != it.m_bot || m_index != it.m_index; } + + CTFBot *m_bot; + int m_index; + }; + + Iterator GetFirstMember( void ) const; + Iterator GetNextMember( const Iterator &it ) const; + Iterator InvalidIterator() const; + + void CollectMembers( CUtlVector< CTFBot * > *memberVector ) const; + + #define EXCLUDE_LEADER false + float GetSlowestMemberSpeed( bool includeLeader = true ) const; + float GetSlowestMemberIdealSpeed( bool includeLeader = true ) const; + float GetMaxSquadFormationError( void ) const; + + bool ShouldSquadLeaderWaitForFormation( void ) const; // return true if the squad leader needs to wait for members to catch up, ignoring those who have broken ranks + bool IsInFormation( void ) const; // return true if the squad is in formation (everyone is in or nearly in their desired positions) + + float GetFormationSize( void ) const; + void SetFormationSize( float size ); + + void DisbandAndDeleteSquad( void ); + + void SetShouldPreserveSquad( bool bShouldPreserveSquad ) { m_bShouldPreserveSquad = bShouldPreserveSquad; } + bool ShouldPreserveSquad() const { return m_bShouldPreserveSquad; } + +private: + friend class CTFBot; + + void Join( CTFBot *bot ); + void Leave( CTFBot *bot ); + + CUtlVector< CHandle< CTFBot > > m_roster; + CHandle< CTFBot > m_leader; + + float m_formationSize; + bool m_bShouldPreserveSquad; +}; + +inline bool CTFBotSquad::IsMember( CTFBot *bot ) const +{ + return m_roster.HasElement( bot ); +} + +inline bool CTFBotSquad::IsLeader( CTFBot *bot ) const +{ + return m_leader == bot; +} + +inline CTFBotSquad::Iterator CTFBotSquad::InvalidIterator() const +{ + return Iterator( NULL, -1 ); +} + +inline float CTFBotSquad::GetFormationSize( void ) const +{ + return m_formationSize; +} + +inline void CTFBotSquad::SetFormationSize( float size ) +{ + m_formationSize = size; +} + + +#endif // TF_BOT_SQUAD_H + diff --git a/game/server/tf/bot/tf_bot_vision.cpp b/game/server/tf/bot/tf_bot_vision.cpp new file mode 100644 index 0000000..6fae0ff --- /dev/null +++ b/game/server/tf/bot/tf_bot_vision.cpp @@ -0,0 +1,482 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_vision.cpp +// Team Fortress NextBot vision interface +// Michael Booth, May 2009 + +#include "cbase.h" +#include "vprof.h" + +#include "tf_bot.h" +#include "tf_bot_vision.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_obj_sentrygun.h" + +ConVar tf_bot_choose_target_interval( "tf_bot_choose_target_interval", "0.3f", FCVAR_CHEAT, "How often, in seconds, a TFBot can reselect his target" ); +ConVar tf_bot_sniper_choose_target_interval( "tf_bot_sniper_choose_target_interval", "3.0f", FCVAR_CHEAT, "How often, in seconds, a zoomed-in Sniper can reselect his target" ); + + +//------------------------------------------------------------------------------------------ +// Update internal state +void CTFBotVision::Update( void ) +{ + if ( TFGameRules()->IsMannVsMachineMode() ) + { + // Throttle vision update rate of robots in MvM for perf at the expense of reaction times + if ( !m_scanTimer.IsElapsed() ) + { + return; + } + + m_scanTimer.Start( RandomFloat( 0.9f, 1.1f ) ); + } + + IVision::Update(); + + CTFBot *me = (CTFBot *)GetBot()->GetEntity(); + if ( !me ) + return; + + // forget spies we have lost sight of + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, GetEnemyTeam( me->GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS ); + + for( int i=0; i<playerVector.Count(); ++i ) + { + if ( !playerVector[i]->IsPlayerClass( TF_CLASS_SPY ) ) + continue; + + const CKnownEntity *known = GetKnown( playerVector[i] ); + + if ( !known || !known->IsVisibleRecently() ) + { + // if a hidden spy changes disguises, we no longer recognize him + if ( playerVector[i]->m_Shared.InCond( TF_COND_DISGUISING ) ) + { + me->ForgetSpy( playerVector[i] ); + } + } + } +} + + +//------------------------------------------------------------------------------------------ +void CTFBotVision::CollectPotentiallyVisibleEntities( CUtlVector< CBaseEntity * > *potentiallyVisible ) +{ + VPROF_BUDGET( "CTFBotVision::CollectPotentiallyVisibleEntities", "NextBot" ); + + potentiallyVisible->RemoveAll(); + + // include all players + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = UTIL_PlayerByIndex( i ); + + if ( player == NULL ) + continue; + + if ( FNullEnt( player->edict() ) ) + continue; + + if ( !player->IsPlayer() ) + continue; + + if ( !player->IsConnected() ) + continue; + + if ( !player->IsAlive() ) + continue; + + potentiallyVisible->AddToTail( player ); + } + + // include sentry guns + UpdatePotentiallyVisibleNPCVector(); + + FOR_EACH_VEC( m_potentiallyVisibleNPCVector, it ) + { + potentiallyVisible->AddToTail( m_potentiallyVisibleNPCVector[ it ] ); + } +} + + +//------------------------------------------------------------------------------------------ +void CTFBotVision::UpdatePotentiallyVisibleNPCVector( void ) +{ + if ( m_potentiallyVisibleUpdateTimer.IsElapsed() ) + { + m_potentiallyVisibleUpdateTimer.Start( RandomFloat( 3.0f, 4.0f ) ); + + // collect list of active buildings + m_potentiallyVisibleNPCVector.RemoveAll(); + + bool bShouldSeeTeleporter = !TFGameRules()->IsMannVsMachineMode() || GetBot()->GetEntity()->GetTeamNumber() != TF_TEAM_PVE_INVADERS; + for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i ) + { + CBaseObject* pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] ); + if ( pObj->ObjectType() == OBJ_SENTRYGUN ) + { + m_potentiallyVisibleNPCVector.AddToTail( pObj ); + } + else if ( pObj->ObjectType() == OBJ_DISPENSER && pObj->ClassMatches( "obj_dispenser" ) ) + { + m_potentiallyVisibleNPCVector.AddToTail( pObj ); + } + else if ( bShouldSeeTeleporter && pObj->ObjectType() == OBJ_TELEPORTER ) + { + m_potentiallyVisibleNPCVector.AddToTail( pObj ); + } + } + + CUtlVector< INextBot * > botVector; + TheNextBots().CollectAllBots( &botVector ); + for( int i=0; i<botVector.Count(); ++i ) + { + CBaseCombatCharacter *botEntity = botVector[i]->GetEntity(); + if ( botEntity && !botEntity->IsPlayer() ) + { + // NPC + m_potentiallyVisibleNPCVector.AddToTail( botEntity ); + } + } + } +} + + +//------------------------------------------------------------------------------------------ +/** + * Return true to completely ignore this entity. + * This is mostly for enemy spies. If we don't ignore them, we will look at them. + */ +bool CTFBotVision::IsIgnored( CBaseEntity *subject ) const +{ + CTFBot *me = (CTFBot *)GetBot()->GetEntity(); + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() ) + { + if ( me->IsPlayerClass( TF_CLASS_SCOUT ) ) + { + // Scouts are wandering defenders, and aggro purely on proximity or damage, not vision + return true; + } + } +#endif // TF_RAID_MODE + + if ( me->IsAttentionFocused() ) + { + // our attention is restricted to certain subjects + if ( !me->IsAttentionFocusedOn( subject ) ) + { + return false; + } + } + + if ( !me->IsEnemy( subject ) ) + { + // don't ignore friends + return false; + } + + if ( subject->IsEffectActive( EF_NODRAW ) ) + { + return true; + } + + if ( subject->IsPlayer() ) + { + CTFPlayer *enemy = static_cast< CTFPlayer * >( subject ); + + // test for designer-defined ignorance + switch( enemy->GetPlayerClass()->GetClassIndex() ) + { + case TF_CLASS_MEDIC: + if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_MEDICS ) ) + { + return true; + } + break; + + case TF_CLASS_ENGINEER: + if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_ENGINEERS ) ) + { + return true; + } + break; + + case TF_CLASS_SNIPER: + if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_SNIPERS ) ) + { + return true; + } + break; + + case TF_CLASS_SCOUT: + if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_SCOUTS ) ) + { + return true; + } + break; + + case TF_CLASS_SPY: + if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_SPIES ) ) + { + return true; + } + break; + + case TF_CLASS_DEMOMAN: + if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_DEMOMEN ) ) + { + return true; + } + break; + + case TF_CLASS_SOLDIER: + if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_SOLDIERS ) ) + { + return true; + } + break; + + case TF_CLASS_HEAVYWEAPONS: + if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_HEAVIES ) ) + { + return true; + } + break; + + case TF_CLASS_PYRO: + if ( me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_PYROS ) ) + { + return true; + } + break; + } + +#ifdef STAGING_ONLY + if ( enemy->m_Shared.InCond( TF_COND_REPROGRAMMED ) ) + { + return true; + } +#endif // STAGING_ONLY + + if ( me->IsKnownSpy( enemy ) ) + { + // don't ignore revealed spies + return false; + } + + if ( enemy->m_Shared.InCond( TF_COND_BURNING ) || + enemy->m_Shared.InCond( TF_COND_URINE ) || + enemy->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) || + enemy->m_Shared.InCond( TF_COND_BLEEDING ) ) + { + // always notice players with these conditions + return false; + } + + // An upgrade in MvM grants AE stealth where the player can fire + // while in stealth, and for a short period after it drops + if ( enemy->m_Shared.InCond( TF_COND_STEALTHED_USER_BUFF_FADING ) ) + { + return true; + } + + if ( enemy->m_Shared.IsStealthed() ) + { + if ( enemy->m_Shared.GetPercentInvisible() < 0.75f ) + { + // spy is partially cloaked, and therefore attracts our attention + return false; + } + + // invisible! + return true; + } + + if ( enemy->IsPlacingSapper() ) + { + return false; + } + + if ( enemy->m_Shared.InCond( TF_COND_DISGUISING ) ) + { + return false; + } + + if ( enemy->m_Shared.InCond( TF_COND_DISGUISED ) && enemy->m_Shared.GetDisguiseTeam() == me->GetTeamNumber() ) + { + // spy is disguised as a member of my team + return true; + } + } + else if ( subject->IsBaseObject() ) // not a player + { + CBaseObject *object = assert_cast< CBaseObject * >( subject ); + if ( object ) + { + // ignore sapped enemy objects + if ( object->HasSapper() ) + { + // unless we're in MvM where buildings can have really large health pools, + // so an engineer can die and run back in time to repair their stuff + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + return false; + } + + return true; + } + + // ignore carried objects + if ( object->IsPlacing() || object->IsCarried() ) + { + return true; + } + + if ( object->GetType() == OBJ_SENTRYGUN && me->IsBehaviorFlagSet( TFBOT_IGNORE_ENEMY_SENTRY_GUNS ) ) + { + return true; + } + } + } + + return false; +} + + +//------------------------------------------------------------------------------------------ +// Return true if we 'notice' the subject, even though we have LOS to it +bool CTFBotVision::IsVisibleEntityNoticed( CBaseEntity *subject ) const +{ + CTFBot *me = (CTFBot *)GetBot()->GetEntity(); + + if ( subject->IsPlayer() && me->IsEnemy( subject ) ) + { + CTFPlayer *player = static_cast< CTFPlayer * >( subject ); + + if ( player->m_Shared.InCond( TF_COND_BURNING ) || + player->m_Shared.InCond( TF_COND_URINE ) || + player->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) || + player->m_Shared.InCond( TF_COND_BLEEDING ) ) + { + // always notice players with these conditions + if ( player->m_Shared.InCond( TF_COND_STEALTHED ) ) + { + me->RealizeSpy( player ); + } + return true; + } + +#ifdef STAGING_ONLY + // Bots can be hacked/reprogrammed by spies. Ignore. + if ( player->m_Shared.InCond( TF_COND_REPROGRAMMED ) ) + { + return false; + } +#endif // STAGING_ONLY + + // An upgrade in MvM grants AE stealth where the player can fire + // while in stealth, and for a short period after it drops + if ( player->m_Shared.InCond( TF_COND_STEALTHED_USER_BUFF_FADING ) ) + { + me->ForgetSpy( player ); + return false; + } + + if ( player->m_Shared.IsStealthed() ) + { + if ( player->m_Shared.GetPercentInvisible() < 0.75f ) + { + // spy is partially cloaked, and therefore attracts our attention + me->RealizeSpy( player ); + return true; + } + + // invisible! + me->ForgetSpy( player ); + return false; + } + + if ( TFGameRules()->IsMannVsMachineMode() ) // in MvM mode, forget spies as soon as they are fully disguised + { + CTFBot::SuspectedSpyInfo_t* pSuspectInfo = me->IsSuspectedSpy( player ); + // But only if we aren't suspecting them currently. This happens when we bump into them. + if( !pSuspectInfo || !pSuspectInfo->IsCurrentlySuspected() ) + { + if ( player->m_Shared.InCond( TF_COND_DISGUISED ) && player->m_Shared.GetDisguiseTeam() == me->GetTeamNumber() ) + { + me->ForgetSpy( player ); + return false; + } + } + } + + if ( me->IsKnownSpy( player ) ) + { + // always notice non-invisible revealed spies + return true; + } + + if ( !TFGameRules()->IsMannVsMachineMode() ) // ignore in MvM mode + { + if ( player->IsPlacingSapper() ) + { + // spotted a spy! + me->RealizeSpy( player ); + return true; + } + } + + if ( player->m_Shared.InCond( TF_COND_DISGUISING ) ) + { + // spotted a spy! + me->RealizeSpy( player ); + return true; + } + + if ( player->m_Shared.InCond( TF_COND_DISGUISED ) && player->m_Shared.GetDisguiseTeam() == me->GetTeamNumber() ) + { + // spy is disguised as a member of my team, don't notice him + return false; + } + } + + return true; +} + + +//------------------------------------------------------------------------------------------ +// Return VISUAL reaction time +float CTFBotVision::GetMinRecognizeTime( void ) const +{ + CTFBot *me = (CTFBot *)GetBot(); + + switch ( me->GetDifficulty() ) + { + case CTFBot::EASY: return 1.0f; + case CTFBot::NORMAL: return 0.5f; + case CTFBot::HARD: return 0.3f; + case CTFBot::EXPERT: return 0.2f; + } + + return 1.0f; +} + + + +//------------------------------------------------------------------------------------------ +float CTFBotVision::GetMaxVisionRange( void ) const +{ + CTFBot *me = (CTFBot *)GetBot(); + + if ( me->GetMaxVisionRangeOverride() > 0.0f ) + { + // designer specified vision range + return me->GetMaxVisionRangeOverride(); + } + + // long range, particularly for snipers + return 6000.0f; +} diff --git a/game/server/tf/bot/tf_bot_vision.h b/game/server/tf/bot/tf_bot_vision.h new file mode 100644 index 0000000..7f999e5 --- /dev/null +++ b/game/server/tf/bot/tf_bot_vision.h @@ -0,0 +1,44 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_bot_vision.h +// Team Fortress NextBot vision interface +// Michael Booth, May 2009 + +#ifndef TF_BOT_VISION_H +#define TF_BOT_VISION_H + +#include "NextBotVisionInterface.h" + +//---------------------------------------------------------------------------- +class CTFBotVision : public IVision +{ +public: + CTFBotVision( INextBot *bot ) : IVision( bot ) + { + } + + virtual ~CTFBotVision() { } + + virtual void Update( void ); // update internal state + + /** + * Populate "potentiallyVisible" with the set of all entities we could potentially see. + * Entities in this set will be tested for visibility/recognition in IVision::Update() + */ + virtual void CollectPotentiallyVisibleEntities( CUtlVector< CBaseEntity * > *potentiallyVisible ); + + virtual bool IsIgnored( CBaseEntity *subject ) const; // return true to completely ignore this entity (may not be in sight when this is called) + virtual bool IsVisibleEntityNoticed( CBaseEntity *subject ) const; // return true if we 'notice' the subject, even though we have LOS to it + + virtual float GetMaxVisionRange( void ) const; // return maximum distance vision can reach + virtual float GetMinRecognizeTime( void ) const; // return VISUAL reaction time + +private: + CUtlVector< CHandle< CBaseCombatCharacter > > m_potentiallyVisibleNPCVector; + CountdownTimer m_potentiallyVisibleUpdateTimer; + void UpdatePotentiallyVisibleNPCVector( void ); + + CountdownTimer m_scanTimer; +}; + + +#endif // TF_BOT_VISION_H
\ No newline at end of file |