//========= 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; pIsBaseObject() ) { // 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; im_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; iIsEnabled() && 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; jGetType() == 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; iGetID() == 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; }