From 0134d137a55dde63de23e8d69b93066467effbbd Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 11 Jul 2026 02:25:26 -0600 Subject: [PATCH] Bots avoid hazard areas Bots avoid hazard areas: - areas that are in line of sight of a smoke cloud - areas that are in line of sight of a grenade trajectory --- src/game/server/CMakeLists.txt | 1 + .../server/NextBot/NextBotVisionInterface.cpp | 29 ++ .../neo/bot/behavior/neo_bot_attack.cpp | 21 +- .../neo/bot/behavior/neo_bot_ctg_escort.cpp | 2 +- .../bot/behavior/neo_bot_grenade_dispatch.cpp | 46 ++-- .../behavior/neo_bot_grenade_throw_frag.cpp | 11 + .../behavior/neo_bot_retreat_from_grenade.cpp | 35 +++ .../neo_bot_retreat_from_hazard_area.cpp | 150 +++++++++++ .../neo_bot_retreat_from_hazard_area.h | 27 ++ .../bot/behavior/neo_bot_retreat_to_cover.cpp | 23 +- .../bot/behavior/neo_bot_tactical_monitor.cpp | 18 ++ .../bot/behavior/neo_bot_tactical_monitor.h | 1 + .../server/neo/bot/neo_bot_path_compute.cpp | 11 +- .../server/neo/bot/neo_bot_path_compute.h | 2 +- src/game/server/neo/bot/neo_bot_path_cost.cpp | 249 +++++++++--------- .../neo/bot/neo_bot_path_reservation.cpp | 201 +++++++++++++- .../server/neo/bot/neo_bot_path_reservation.h | 15 ++ src/game/shared/neo/neo_gamerules.cpp | 5 + 18 files changed, 693 insertions(+), 154 deletions(-) create mode 100644 src/game/server/neo/bot/behavior/neo_bot_retreat_from_hazard_area.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_retreat_from_hazard_area.h diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 5f9434ff9a..e37fec41dc 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1541,6 +1541,7 @@ set(UNITY_SOURCE_NEO_BOT neo/bot/behavior/neo_bot_pause.cpp neo/bot/behavior/neo_bot_retreat_to_cover.cpp neo/bot/behavior/neo_bot_retreat_from_grenade.cpp + neo/bot/behavior/neo_bot_retreat_from_hazard_area.cpp neo/bot/behavior/neo_bot_scenario_monitor.cpp neo/bot/behavior/neo_bot_seek_and_destroy.cpp neo/bot/behavior/neo_bot_seek_weapon.cpp diff --git a/src/game/server/NextBot/NextBotVisionInterface.cpp b/src/game/server/NextBot/NextBotVisionInterface.cpp index 9523fbdfbe..335aecdb0d 100644 --- a/src/game/server/NextBot/NextBotVisionInterface.cpp +++ b/src/game/server/NextBot/NextBotVisionInterface.cpp @@ -21,6 +21,8 @@ #include "neo_player.h" #include "neo_smokelineofsightblocker.h" #include "bot/neo_bot.h" +#include "bot/neo_bot_path_reservation.h" +#include "nav_mesh.h" #endif #include "tier0/vprof.h" @@ -794,6 +796,33 @@ bool IVision::IsLineOfSightClearToEntity( const CBaseEntity *subject, Vector *vi UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), firstPosition, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result ); if ( result.DidHit() ) { + // Check if first center traceline hit any blocking smoke + if (neo_bot_path_reservation_enable.GetBool() + && result.m_pEnt + && FStrEq(result.m_pEnt->GetClassname(), SMOKELINEOFSIGHTBLOCKER_ENTITYNAME)) + { + auto pSmoke = static_cast(result.m_pEnt); + // Assume that threat could be hiding inside navarea where smoke blocked sight + if (neoBot) + { + int team = neoBot->GetTeamNumber(); + + CNavArea *smokeArea = TheNavMesh->GetNearestNavArea(result.endpos); + if (smokeArea) + { + // Register smoke sightlines for team to avoid as hazardous + CNEOBotPathReservations()->AddSmokeHazard(smokeArea->GetID(), pSmoke->GetNextThink(), team); + } + + CNavArea *myArea = neoBot->GetLastKnownArea(); + if (myArea) + { + // Also register the area I saw the smoke from as hazardous + CNEOBotPathReservations()->AddSmokeHazard(myArea->GetID(), pSmoke->GetNextThink(), team, false); + } + } + } + UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), secondPosition, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result ); if ( result.DidHit() ) diff --git a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp index cc13574b3e..98ce6931f5 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp @@ -62,6 +62,7 @@ class CSearchForAttackCover : public ISearchSurroundingAreasFunctor m_threatArea = threat->GetLastKnownArea(); m_goalArea = goalArea ? goalArea : m_threatArea; // prioritize movement towards input goal area or threat m_myDistToGoalSq = m_goalArea ? ( m_goalArea->GetCenter() - m_me->GetAbsOrigin() ).LengthSqr() : 0; + m_onStuckPenalty = neo_bot_path_reservation_onstuck_penalty.GetFloat(); } virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) @@ -79,10 +80,18 @@ class CSearchForAttackCover : public ISearchSurroundingAreasFunctor return true; // skip our starting area } - if ( neo_bot_path_reservation_enable.GetBool() && - ( CNEOBotPathReservations()->GetAreaAvoidPenalty(area->GetID()) > 0 ) ) + // Skip areas that are hazardous or where bots get stuck + if ( neo_bot_path_reservation_enable.GetBool() ) { - return true; // skip areas that have had navigation hiccups + int navAreaId = area->GetID(); + if (CNEOBotPathReservations()->IsAreaHazardous(navAreaId, m_me)) + { + return true; + } + if (CNEOBotPathReservations()->GetAreaAvoidPenalty(navAreaId) >= m_onStuckPenalty) + { + return true; + } } if ( m_attackCoverArea ) @@ -199,6 +208,7 @@ class CSearchForAttackCover : public ISearchSurroundingAreasFunctor const CNavArea *m_myArea; // reference point of myself const CNavArea *m_threatArea; // reference point of the threat float m_myDistToGoalSq; // the bot's current distance to the threat + float m_onStuckPenalty; // cache onstuck penalty }; @@ -452,11 +462,6 @@ QueryResultType CNEOBotAttack::ShouldRetreat( const INextBot *me ) const return ANSWER_UNDEFINED; } - if ( m_goalArea && m_attackCoverArea ) - { - return ANSWER_NO; - } - return ANSWER_UNDEFINED; } diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp index 077b9143e3..630eda2700 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp @@ -196,7 +196,7 @@ ActionResult< CNEOBot > CNEOBotCtgEscort::Update( CNEOBot *me, float interval ) m_chasePath.Invalidate(); CNEOBotPathCompute( me, m_path, vecMoveTarget, SAFEST_ROUTE ); - m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + m_repathTimer.Start( RandomFloat( 5.0f, 10.0f ) ); } m_path.Update( me ); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp index 4934908a14..e3b296a641 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp @@ -9,6 +9,8 @@ #include "weapon_grenade.h" #include "weapon_smokegrenade.h" +ConVar sv_neo_bot_grenade_polite_frag("sv_neo_bot_grenade_polite_frag", "1", FCVAR_NONE, "Force bots to consider teammates when throwing frag grenades.", true, 0, true, 1); +ConVar sv_neo_bot_grenade_polite_smoke("sv_neo_bot_grenade_polite_smoke", "1", FCVAR_NONE, "Force bots to consider teammates when throwing smoke grenades.", true, 0, true, 1); ConVar sv_neo_bot_grenade_use_frag("sv_neo_bot_grenade_use_frag", "1", FCVAR_NONE, "Allow bots to use frag grenades", true, 0, true, 1); ConVar sv_neo_bot_grenade_use_smoke("sv_neo_bot_grenade_use_smoke", "1", FCVAR_NONE, "Allow bots to use smoke grenades", true, 0, true, 1); ConVar sv_neo_bot_grenade_throw_cooldown("sv_neo_bot_grenade_throw_cooldown", "10", FCVAR_NONE, "Cooldown in seconds between grenade throws for bots"); @@ -78,29 +80,32 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( const CNE // Should I toss a smoke grenade? if ( pSmokeGrenade && (me->GetClass() == NEO_CLASS_SUPPORT) ) { - for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + if ( sv_neo_bot_grenade_polite_smoke.GetBool() ) { - CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); - if ( !pPlayer || !pPlayer->IsAlive() || pPlayer == me ) + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) { - continue; - } + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pPlayer || !pPlayer->IsAlive() || pPlayer == me ) + { + continue; + } - if ( pPlayer->InSameTeam( me ) ) - { - if ( !pPlayer->IsBot() && pPlayer->GetClass() != NEO_CLASS_SUPPORT ) + if ( pPlayer->InSameTeam( me ) ) { - // Avoid blocking the vision of a friendly human - // (Bots benefit from concealment without the disorientation) - return nullptr; + if ( !pPlayer->IsBot() && pPlayer->GetClass() != NEO_CLASS_SUPPORT ) + { + // Avoid blocking the vision of a friendly human + // (Bots benefit from concealment without the disorientation) + return nullptr; + } } - } - else - { - if ( pPlayer->GetClass() == NEO_CLASS_SUPPORT ) + else { - // Avoid giving an enemy with thermal vision a free smoke screen - return nullptr; + if ( pPlayer->GetClass() == NEO_CLASS_SUPPORT ) + { + // Avoid giving an enemy with thermal vision a free smoke screen + return nullptr; + } } } } @@ -111,9 +116,12 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( const CNE // Should I toss a frag grenade? if ( pFragGrenade ) { - if ( !CNEOBotGrenadeThrowFrag::IsFragSafe( me, threat->GetLastKnownPosition() ) ) + if ( sv_neo_bot_grenade_polite_frag.GetBool() ) { - return nullptr; + if ( !CNEOBotGrenadeThrowFrag::IsFragSafe( me, threat->GetLastKnownPosition() ) ) + { + return nullptr; + } } return new CNEOBotGrenadeThrowFrag( pFragGrenade, threat ); diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp index f9b215c1a6..893d5b7e03 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp @@ -1,11 +1,13 @@ #include "cbase.h" #include "bot/neo_bot.h" #include "bot/neo_bot_path_compute.h" +#include "bot/neo_bot_path_reservation.h" #include "bot/behavior/neo_bot_grenade_throw_frag.h" #include "neo_gamerules.h" #include "neo_player.h" #include "weapon_neobasecombatweapon.h" +#include "nav_mesh.h" #include "nav_pathfind.h" extern ConVar sv_neo_grenade_blast_radius; @@ -106,6 +108,15 @@ CNEOBotGrenadeThrow::ThrowTargetResult CNEOBotGrenadeThrowFrag::UpdateGrenadeTar return THROW_TARGET_CANCEL; // risk of friendly fire } + // Register explosive hazard at the target position + // so the throwing bot's team can avoid the trajectory and landing areas + CNavArea *area = TheNavMesh->GetNearestNavArea(m_vecTarget); + if (area) + { + int team = me->GetTeamNumber(); + CNEOBotPathReservations()->AddFragHazard(area->GetID(), gpGlobals->curtime + sv_neo_grenade_fuse_timer.GetFloat(), team); + } + return THROW_TARGET_READY; } diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp index e98035eaf6..4a7f55b64b 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp @@ -5,6 +5,8 @@ #include "bot/behavior/neo_bot_retreat_from_grenade.h" #include "bot/behavior/neo_bot_retreat_to_cover.h" #include "bot/neo_bot_path_compute.h" +#include "bot/neo_bot_path_reservation.h" +#include "nav_mesh.h" #include "sdk/sdk_basegrenade_projectile.h" // memdbgon must be the last include file in a .cpp file!!! @@ -72,6 +74,7 @@ class CSearchForCoverFromGrenade : public ISearchSurroundingAreasFunctor { m_me = me; m_grenade = grenade; + m_onStuckPenalty = neo_bot_path_reservation_onstuck_penalty.GetFloat(); m_pGrenadeStats = dynamic_cast( grenade ); m_safeRadiusSqr = m_pGrenadeStats ? Square(m_pGrenadeStats->m_DmgRadius * sv_neo_bot_grenade_frag_safety_range_multiplier.GetFloat()) : 0.0f; @@ -91,6 +94,20 @@ class CSearchForCoverFromGrenade : public ISearchSurroundingAreasFunctor return false; // can't search if there's no threat source } + // Skip areas that are hazardous or where bots get stuck + if ( neo_bot_path_reservation_enable.GetBool() ) + { + int navAreaId = area->GetID(); + if (CNEOBotPathReservations()->IsAreaHazardous(navAreaId, m_me)) + { + return true; + } + if (CNEOBotPathReservations()->GetAreaAvoidPenalty(navAreaId) >= m_onStuckPenalty) + { + return true; + } + } + if ( m_pGrenadeStats ) { if ( ( area->GetCenter() - m_pGrenadeStats->GetAbsOrigin() ).LengthSqr() < m_safeRadiusSqr * 2 ) @@ -138,6 +155,7 @@ class CSearchForCoverFromGrenade : public ISearchSurroundingAreasFunctor CNEOBot *m_me; CBaseEntity *m_grenade; CBaseGrenadeProjectile *m_pGrenadeStats; + float m_onStuckPenalty; float m_safeRadiusSqr; CUtlVector< CNavArea * > m_coverAreaVector; }; @@ -181,6 +199,19 @@ ActionResult< CNEOBot > CNEOBotRetreatFromGrenade::OnStart( CNEOBot *me, Action< m_grenade = FindDangerousGrenade( me ); } + if ( !m_grenade ) // not a duplicate, check FindDangerousGrenade result + { + return Done("No grenade found"); + } + + // Register explosive hazard for the bot's team at the grenade's initial position + CNavArea *grenadeArea = TheNavMesh->GetNearestNavArea( m_grenade->GetAbsOrigin() ); + if (grenadeArea) + { + int team = me->GetTeamNumber(); + CNEOBotPathReservations()->AddFragHazard(grenadeArea->GetID(), gpGlobals->curtime + sv_neo_grenade_fuse_timer.GetFloat(), team); + } + // Sometimes grenades can be in a bad limbo state, so force exit eventually m_expiryTimer.Start( sv_neo_grenade_fuse_timer.GetFloat() ); @@ -247,6 +278,8 @@ ActionResult< CNEOBot > CNEOBotRetreatFromGrenade::Update( CNEOBot *me, float in //--------------------------------------------------------------------------------------------- EventDesiredResult< CNEOBot > CNEOBotRetreatFromGrenade::OnStuck( CNEOBot *me ) { + m_coverArea = FindCoverArea( me ); + CNEOBotPathCompute(me, m_path, m_coverArea->GetCenter(), FASTEST_ROUTE); return TryContinue(); } @@ -261,6 +294,8 @@ EventDesiredResult< CNEOBot > CNEOBotRetreatFromGrenade::OnMoveToSuccess( CNEOBo //--------------------------------------------------------------------------------------------- EventDesiredResult< CNEOBot > CNEOBotRetreatFromGrenade::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) { + m_coverArea = FindCoverArea( me ); + CNEOBotPathCompute(me, m_path, m_coverArea->GetCenter(), FASTEST_ROUTE); return TryContinue(); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_from_hazard_area.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_from_hazard_area.cpp new file mode 100644 index 0000000000..a77bdc68cf --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_from_hazard_area.cpp @@ -0,0 +1,150 @@ +#include "cbase.h" +#include "neo_bot_retreat_from_hazard_area.h" +#include "neo_bot_retreat_from_grenade.h" +#include "neo_bot_retreat_to_cover.h" +#include "neo/bot/neo_bot_path_compute.h" +#include "neo/bot/neo_bot_path_reservation.h" + +const int MAX_NON_HAZARD_AREA_CANDIDATES = 5; + +class CSearchForSafeArea : public ISearchSurroundingAreasFunctor +{ +public: + CSearchForSafeArea(CNEOBot *me) + : m_me(me) + , m_onStuckPenalty(neo_bot_path_reservation_onstuck_penalty.GetFloat()) + { + } + + virtual bool operator()(CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar) + { + int id = baseArea->GetID(); + float candidateHazardTime = CNEOBotPathReservations()->GetAreaHazardousTime(id, m_me); + if (candidateHazardTime > 0) + { + if ( m_me->IsDebugging( NEXTBOT_PATH ) ) + { + baseArea->DrawFilled(255, 0, 0, MIN(255, candidateHazardTime), candidateHazardTime); + } + } + else if (CNEOBotPathReservations()->GetAreaAvoidPenalty(id) < m_onStuckPenalty) + { + m_safeAreas.AddToTail(baseArea); + } + + if (m_safeAreas.Count() >= MAX_NON_HAZARD_AREA_CANDIDATES) + { + return false; // found enough candidates + } + + return true; + } + + virtual bool ShouldSearch(CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar) + { + return (currentArea->ComputeAdjacentConnectionHeightChange(adjArea) < + m_me->GetLocomotionInterface()->GetMaxJumpHeight()); + } + + CUtlVector m_safeAreas; + +private: + CNEOBot *m_me; + float m_onStuckPenalty; +}; + +CNEOBotRetreatFromHazardArea::CNEOBotRetreatFromHazardArea() +{ +} + +CNavArea *CNEOBotRetreatFromHazardArea::FindSafeArea(CNEOBot *me) +{ + CNavArea *start = me->GetLastKnownArea(); + if (!start) + { + return nullptr; + } + + CSearchForSafeArea search(me); + SearchSurroundingAreas(start, search); + + if (search.m_safeAreas.Count() == 0) + { + return nullptr; + } + + int last = MIN(MAX_NON_HAZARD_AREA_CANDIDATES, search.m_safeAreas.Count()); + int which = RandomInt(0, last - 1); + return search.m_safeAreas[which]; +} + +ActionResult CNEOBotRetreatFromHazardArea::OnStart(CNEOBot *me, Action *priorAction) +{ + m_safeArea = FindSafeArea(me); + + if (!m_safeArea) + { + return Done("No safe area available!"); + } + + m_path.SetMinLookAheadDistance(me->GetDesiredPathLookAheadRange()); + m_repathTimer.Invalidate(); + + return Continue(); +} + +ActionResult CNEOBotRetreatFromHazardArea::Update(CNEOBot *me, float interval) +{ + CBaseEntity *dangerousGrenade = CNEOBotRetreatFromGrenade::FindDangerousGrenade(me); + if (dangerousGrenade) + { + return ChangeTo(new CNEOBotRetreatFromGrenade(dangerousGrenade), "Encountered grenade while avoiding hazard!"); + } + + CNavArea *myArea = me->GetLastKnownArea(); + if (!myArea) + { + return Done("Can't identify my nav area!"); + } + + if ( !CNEOBotPathReservations()->IsAreaHazardous(myArea->GetID(), me)) + { + return Done("Left hazardous area"); + } + + if ( (myArea->GetID() == m_safeArea->GetID()) || (CNEOBotPathReservations()->IsAreaHazardous(m_safeArea->GetID(), me)) ) + { + // Safe area is no longer safe at this point, look for new candidate + m_safeArea = FindSafeArea(me); + m_repathTimer.Invalidate(); + } + + if (!m_safeArea) + { + return Done("Could not find replacement safe area!"); + } + + if (m_repathTimer.IsElapsed()) + { + m_repathTimer.Start(10.0f); + // RETREAT_ROUTE: Allow pathing through hazardous areas in CNEOBotPathCost + CNEOBotPathCompute(me, m_path, m_safeArea->GetCenter(), RETREAT_ROUTE); + } + + m_path.Update(me); + return Continue(); +} + +EventDesiredResult< CNEOBot > CNEOBotRetreatFromHazardArea::OnStuck( CNEOBot *me ) +{ + m_safeArea = FindSafeArea(me); + CNEOBotPathCompute(me, m_path, m_safeArea->GetCenter(), RETREAT_ROUTE); + return TryContinue(); +} + +EventDesiredResult< CNEOBot > CNEOBotRetreatFromHazardArea::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +{ + m_safeArea = FindSafeArea(me); + CNEOBotPathCompute(me, m_path, m_safeArea->GetCenter(), RETREAT_ROUTE); + return TryContinue(); +} \ No newline at end of file diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_from_hazard_area.h b/src/game/server/neo/bot/behavior/neo_bot_retreat_from_hazard_area.h new file mode 100644 index 0000000000..e6ba7c444c --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_from_hazard_area.h @@ -0,0 +1,27 @@ +#pragma once + +#include "NextBotBehavior.h" +#include "nav_mesh.h" +#include "neo/bot/neo_bot.h" +#include "neo/bot/neo_bot_path_compute.h" + +class CNEOBotRetreatFromHazardArea : public Action +{ +public: + CNEOBotRetreatFromHazardArea(); + + virtual ActionResult OnStart(CNEOBot *me, Action *priorAction) OVERRIDE; + virtual ActionResult Update(CNEOBot *me, float interval) OVERRIDE; + + virtual EventDesiredResult OnStuck(CNEOBot *me) OVERRIDE; + virtual EventDesiredResult OnMoveToFailure(CNEOBot *me, const Path *path, MoveToFailureType reason) OVERRIDE; + + virtual const char *GetName() const OVERRIDE { return "RetreatFromHazardArea"; } + +private: + CNavArea *m_safeArea; + CountdownTimer m_repathTimer; + PathFollower m_path; + + CNavArea *FindSafeArea(CNEOBot *me); +}; diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp index b31b8a5ae4..534bb69ae9 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp @@ -8,6 +8,7 @@ #include "bot/behavior/neo_bot_retreat_from_grenade.h" #include "bot/behavior/neo_bot_retreat_to_cover.h" #include "bot/neo_bot_path_compute.h" +#include "bot/neo_bot_path_reservation.h" extern ConVar neo_bot_path_lookahead_range; ConVar neo_bot_retreat_to_cover_range( "neo_bot_retreat_to_cover_range", "1000", FCVAR_CHEAT ); @@ -103,6 +104,7 @@ class CSearchForCover : public ISearchSurroundingAreasFunctor CSearchForCover( CNEOBot *me ) { m_me = me; + m_onStuckPenalty = neo_bot_path_reservation_onstuck_penalty.GetFloat(); m_minExposureCount = 9999; if ( neo_bot_debug_retreat_to_cover.GetBool() ) @@ -115,6 +117,20 @@ class CSearchForCover : public ISearchSurroundingAreasFunctor CNavArea *area = (CNavArea *)baseArea; + // Skip areas that are hazardous or where bots get stuck + if ( neo_bot_path_reservation_enable.GetBool() ) + { + int navAreaId = area->GetID(); + if (CNEOBotPathReservations()->IsAreaHazardous(navAreaId, m_me)) + { + return true; + } + if (CNEOBotPathReservations()->GetAreaAvoidPenalty(navAreaId) >= m_onStuckPenalty) + { + return true; + } + } + CTestAreaAgainstThreats test( m_me, area ); m_me->GetVisionInterface()->ForEachKnownEntity( test ); @@ -156,6 +172,7 @@ class CSearchForCover : public ISearchSurroundingAreasFunctor CNEOBot *m_me; CUtlVector< CNavArea * > m_coverAreaVector; int m_minExposureCount; + float m_onStuckPenalty; }; @@ -311,7 +328,7 @@ ActionResult< CNEOBot > CNEOBotRetreatToCover::Update( CNEOBot *me, float interv if ( m_repathTimer.IsElapsed() ) { - m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); + m_repathTimer.Start( RandomFloat( 5.0f, 10.0f ) ); CNEOBotPathCompute( me, m_path, m_coverArea->GetCenter(), RETREAT_ROUTE ); } @@ -326,6 +343,8 @@ ActionResult< CNEOBot > CNEOBotRetreatToCover::Update( CNEOBot *me, float interv //--------------------------------------------------------------------------------------------- EventDesiredResult< CNEOBot > CNEOBotRetreatToCover::OnStuck( CNEOBot *me ) { + m_coverArea = FindCoverArea( me ); + CNEOBotPathCompute( me, m_path, m_coverArea->GetCenter(), RETREAT_ROUTE ); return TryContinue(); } @@ -340,6 +359,8 @@ EventDesiredResult< CNEOBot > CNEOBotRetreatToCover::OnMoveToSuccess( CNEOBot *m //--------------------------------------------------------------------------------------------- EventDesiredResult< CNEOBot > CNEOBotRetreatToCover::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) { + m_coverArea = FindCoverArea( me ); + CNEOBotPathCompute( me, m_path, m_coverArea->GetCenter(), RETREAT_ROUTE ); return TryContinue(); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp index 78e8be610a..8cde71fc67 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp @@ -7,6 +7,7 @@ #include "bot/neo_bot.h" #include "bot/neo_bot_manager.h" +#include "bot/neo_bot_path_reservation.h" #include "bot/behavior/neo_bot_tactical_monitor.h" #include "bot/behavior/neo_bot_scenario_monitor.h" @@ -15,6 +16,7 @@ #include "bot/behavior/neo_bot_seek_weapon.h" #include "bot/behavior/neo_bot_retreat_to_cover.h" #include "bot/behavior/neo_bot_retreat_from_grenade.h" +#include "bot/behavior/neo_bot_retreat_from_hazard_area.h" #include "bot/behavior/neo_bot_ladder_approach.h" #include "bot/behavior/neo_bot_ladder_climb.h" #include "bot/behavior/neo_bot_path_clear_breakable.h" @@ -81,6 +83,7 @@ Action< CNEOBot > *CNEOBotTacticalMonitor::InitialContainedAction( CNEOBot *me ) ActionResult< CNEOBot > CNEOBotTacticalMonitor::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) { m_pIgnoredWeapons->Reset(); + m_hazardCheckTimer.Start( 0.5f ); return Continue(); } @@ -287,6 +290,21 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter return SuspendFor( new CNEOBotPathClearBreakable( breakable ), "Clearing breakable in path" ); } + // Don't want to interfere with human squad leader's control just to ignore hazards + // Might be annoying if bots ignored following or waypoints (e.g. smoke sightlines avoidance) + if ( !me->m_hCommandingPlayer.Get() && m_hazardCheckTimer.IsElapsed() ) + { + m_hazardCheckTimer.Start( 0.5f ); + CNavArea *myArea = me->GetLastKnownArea(); + if ( myArea ) + { + if ( CNEOBotPathReservations()->IsAreaHazardous( myArea->GetID(), me ) ) + { + return SuspendFor( new CNEOBotRetreatFromHazardArea( ), "Avoiding hazard area" ); + } + } + } + ActionResult< CNEOBot > scavengeResult = ScavengeForPrimaryWeapon( me ); if ( scavengeResult.IsRequestingChange() ) { diff --git a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h index 83634acfcf..352dc5caed 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h +++ b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h @@ -26,6 +26,7 @@ class CNEOBotTacticalMonitor : public Action< CNEOBot > virtual const char* GetName(void) const { return "TacticalMonitor"; } private: + CountdownTimer m_hazardCheckTimer; CountdownTimer m_maintainTimer; CountdownTimer m_acknowledgeAttentionTimer; diff --git a/src/game/server/neo/bot/neo_bot_path_compute.cpp b/src/game/server/neo/bot/neo_bot_path_compute.cpp index 4d2e56f046..0bc1778398 100644 --- a/src/game/server/neo/bot/neo_bot_path_compute.cpp +++ b/src/game/server/neo/bot/neo_bot_path_compute.cpp @@ -39,20 +39,25 @@ static void CNEOBotReservePath(CNEOBot* me, PathFollower& path) } } +// By default, assumes that we would prefer to have partial paths even if we can't make a full path +// to accomodate scenarios like getting as close as possible to a hazard area but not into it +// Set includeGoalIfPathFails to false if you need to get the full path (or nothing on failure) bool CNEOBotPathCompute(CNEOBot* bot, PathFollower& path, const Vector& goal, RouteType route, float maxPathLength, bool includeGoalIfPathFails, bool requireGoalArea) { Assert(goal.IsValid()); CNEOBotPathCost cost_with_reservations(bot, route); - if (path.Compute(bot, goal, cost_with_reservations, maxPathLength, includeGoalIfPathFails, requireGoalArea) && path.IsValid()) + path.Compute(bot, goal, cost_with_reservations, maxPathLength, includeGoalIfPathFails, requireGoalArea); + if (path.IsValid()) { CNEOBotReservePath(bot, path); return true; } - CNEOBotPathCost cost_without_reservations(bot, route); + CNEOBotPathCost cost_without_reservations(bot, FASTEST_ROUTE); cost_without_reservations.m_bIgnoreReservations = true; - if (path.Compute(bot, goal, cost_without_reservations, maxPathLength, includeGoalIfPathFails, requireGoalArea) && path.IsValid()) + path.Compute(bot, goal, cost_without_reservations, maxPathLength, includeGoalIfPathFails, requireGoalArea); + if (path.IsValid()) { CNEOBotReservePath(bot, path); return true; diff --git a/src/game/server/neo/bot/neo_bot_path_compute.h b/src/game/server/neo/bot/neo_bot_path_compute.h index bf9ab2f571..5fea6b7c47 100644 --- a/src/game/server/neo/bot/neo_bot_path_compute.h +++ b/src/game/server/neo/bot/neo_bot_path_compute.h @@ -14,7 +14,7 @@ bool CNEOBotPathCompute const Vector& goal, RouteType route, float maxPathLength = PATH_NO_LENGTH_LIMIT, - bool includeGoalIfPathFails = PATH_TRUNCATE_INCOMPLETE_PATH, + bool includeGoalIfPathFails = true, // return incomplete path if full path not found by default bool requireGoalArea = false ); diff --git a/src/game/server/neo/bot/neo_bot_path_cost.cpp b/src/game/server/neo/bot/neo_bot_path_cost.cpp index 99b6db67a2..81199a0dee 100644 --- a/src/game/server/neo/bot/neo_bot_path_cost.cpp +++ b/src/game/server/neo/bot/neo_bot_path_cost.cpp @@ -55,158 +55,167 @@ float CNEOBotPathCost::operator()(CNavArea* baseArea, CNavArea* fromArea, const // first area in path, no cost return 0.0f; } - else + + if (!m_me->GetLocomotionInterface()->IsAreaTraversable(area)) + { + return -1.0f; + } + + if ( CNEOBotPathReservations()->IsAreaHazardous(area->GetID(), m_me) ) { - if (!m_me->GetLocomotionInterface()->IsAreaTraversable(area)) + if ( m_routeType == DEFAULT_ROUTE ) { + // attempt to route around hazards + // if blocked, assumes includeGoalIfPathFails=true + // to partially path up to boundary of hazard area return -1.0f; } + } - // compute distance traveled along path so far - float dist; - - if (ladder) - { - dist = ladder->m_length; + // compute distance traveled along path so far + float dist; - // ladders leave bots exposed, but can be a shortcut - const float ladderPenalty = neo_bot_path_penalty_ladder_multiplier.GetFloat(); - dist *= ladderPenalty; - } - else if (length > 0.0) - { - dist = length; - } - else - { - dist = (area->GetCenter() - fromArea->GetCenter()).Length(); - } + if (ladder) + { + dist = ladder->m_length; - // Only apply height restrictions for non-ladder jump paths - if (!ladder) - { - // check height change - float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange(area); + // ladders leave bots exposed, but can be a shortcut + const float ladderPenalty = neo_bot_path_penalty_ladder_multiplier.GetFloat(); + dist *= ladderPenalty; + } + else if (length > 0.0) + { + dist = length; + } + else + { + dist = (area->GetCenter() - fromArea->GetCenter()).Length(); + } - if (deltaZ >= m_stepHeight) - { - if (deltaZ >= m_maxJumpHeight) - { - // too high to reach - return -1.0f; - } + // Only apply height restrictions for non-ladder jump paths + if (!ladder) + { + // check height change + float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange(area); - // jumping is slower than flat ground - const float jumpPenalty = neo_bot_path_penalty_jump_multiplier.GetFloat() * Square( deltaZ / m_maxJumpHeight ); - dist *= jumpPenalty; - } - else if (deltaZ < -m_maxDropHeight) + if (deltaZ >= m_stepHeight) + { + if (deltaZ >= m_maxJumpHeight) { - // too far to drop + // too high to reach 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) + // jumping is slower than flat ground + const float jumpPenalty = neo_bot_path_penalty_jump_multiplier.GetFloat() * Square( deltaZ / m_maxJumpHeight ); + dist *= jumpPenalty; + } + else if (deltaZ < -m_maxDropHeight) { - // 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))); + // too far to drop + return -1.0f; } + } - if (m_routeType == SAFEST_ROUTE) - { - // misyl: combat areas. + // 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) + { + // 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) + { + // misyl: combat areas. #if 0 - // avoid combat areas - if (area->IsInCombat()) - { - const float combatDangerCost = 4.0f; - dist *= combatDangerCost * area->GetCombatIntensity(); - } -#endif + // avoid combat areas + if (area->IsInCombat()) + { + const float combatDangerCost = 4.0f; + dist *= combatDangerCost * area->GetCombatIntensity(); } +#endif + } - float cost = (dist * preference); + float cost = (dist * preference); - // ------------------------------------------------------------------------------------------------ - // New path reservation related cost adjustments - if ( !m_bIgnoreReservations && (m_routeType != FASTEST_ROUTE) ) - { - cost += CNEOBotPathReservations()->GetPredictedFriendlyPathCount(area->GetID(), m_me->GetTeamNumber()) * neo_bot_path_reservation_penalty.GetFloat(); - cost += CNEOBotPathReservations()->GetAreaAvoidPenalty(area->GetID()); + // ------------------------------------------------------------------------------------------------ + // New path reservation related cost adjustments + if ( !m_bIgnoreReservations && (m_routeType != FASTEST_ROUTE) ) + { + cost += CNEOBotPathReservations()->GetPredictedFriendlyPathCount(area->GetID(), m_me->GetTeamNumber()) * neo_bot_path_reservation_penalty.GetFloat(); + cost += CNEOBotPathReservations()->GetAreaAvoidPenalty(area->GetID()); - // Weapon range penalties - auto* myWeapon = assert_cast(m_me->GetActiveWeapon()); - if (myWeapon) + // Weapon range penalties + auto* myWeapon = assert_cast(m_me->GetActiveWeapon()); + if (myWeapon) + { + const int nWeaponBits = myWeapon->GetNeoWepBits(); + if (nWeaponBits & NEO_WEP_FIREARM) { - const int nWeaponBits = myWeapon->GetNeoWepBits(); - if (nWeaponBits & NEO_WEP_FIREARM) + const int visibleAreaCount = area->GetPotentiallyVisibleAreaCount(); + if (visibleAreaCount > 0) { - const int visibleAreaCount = area->GetPotentiallyVisibleAreaCount(); - if (visibleAreaCount > 0) + constexpr int nShotgunBits = NEO_WEP_AA13 | NEO_WEP_SUPA7; + constexpr int nBattleRifleBits = NEO_WEP_M41 | NEO_WEP_M41_S; + constexpr int nPistolCaliberBits = NEO_WEP_MILSO | NEO_WEP_TACHI | NEO_WEP_KYLA + | NEO_WEP_MPN | NEO_WEP_MPN_S | NEO_WEP_JITTE | NEO_WEP_JITTE_S | NEO_WEP_SRM | NEO_WEP_SRM_S; + + if (nWeaponBits & nPistolCaliberBits) + { + // Weapons that don't have max first shot accuracy + const float exposurePenalty = neo_bot_path_penalty_exposure_pistol.GetFloat(); + cost += visibleAreaCount * exposurePenalty; + } + else if (nWeaponBits & nShotgunBits) + { + // Weapons that have spread that can't hit long range targets + const float exposurePenalty = neo_bot_path_penalty_exposure_shotgun.GetFloat(); + cost += visibleAreaCount * exposurePenalty; + } + else if (nWeaponBits & nBattleRifleBits) + { + // Weapons that benefit from medium sightlines that can see many NavAreas + const float baseline_penalty = neo_bot_path_penalty_exposure_inverse_base_battle_rifle.GetFloat(); + cost += baseline_penalty / visibleAreaCount; + } + else if (nWeaponBits & NEO_WEP_SCOPEDWEAPON) + { + // Weapons that benefit from long sightlines that can see many NavAreas + const float baseline_penalty = neo_bot_path_penalty_exposure_inverse_base_scoped.GetFloat(); + cost += baseline_penalty / visibleAreaCount; + } + else { - constexpr int nShotgunBits = NEO_WEP_AA13 | NEO_WEP_SUPA7; - constexpr int nBattleRifleBits = NEO_WEP_M41 | NEO_WEP_M41_S; - constexpr int nPistolCaliberBits = NEO_WEP_MILSO | NEO_WEP_TACHI | NEO_WEP_KYLA - | NEO_WEP_MPN | NEO_WEP_MPN_S | NEO_WEP_JITTE | NEO_WEP_JITTE_S | NEO_WEP_SRM | NEO_WEP_SRM_S; - - if (nWeaponBits & nPistolCaliberBits) - { - // Weapons that don't have max first shot accuracy - const float exposurePenalty = neo_bot_path_penalty_exposure_pistol.GetFloat(); - cost += visibleAreaCount * exposurePenalty; - } - else if (nWeaponBits & nShotgunBits) - { - // Weapons that have spread that can't hit long range targets - const float exposurePenalty = neo_bot_path_penalty_exposure_shotgun.GetFloat(); - cost += visibleAreaCount * exposurePenalty; - } - else if (nWeaponBits & nBattleRifleBits) - { - // Weapons that benefit from medium sightlines that can see many NavAreas - const float baseline_penalty = neo_bot_path_penalty_exposure_inverse_base_battle_rifle.GetFloat(); - cost += baseline_penalty / visibleAreaCount; - } - else if (nWeaponBits & NEO_WEP_SCOPEDWEAPON) - { - // Weapons that benefit from long sightlines that can see many NavAreas - const float baseline_penalty = neo_bot_path_penalty_exposure_inverse_base_scoped.GetFloat(); - cost += baseline_penalty / visibleAreaCount; - } - else - { - // Generally avoiding exposed areas when traversing a wide open area - const float exposurePenalty = neo_bot_path_penalty_exposure_base.GetFloat(); - cost += visibleAreaCount * exposurePenalty; - } + // Generally avoiding exposed areas when traversing a wide open area + const float exposurePenalty = neo_bot_path_penalty_exposure_base.GetFloat(); + cost += visibleAreaCount * exposurePenalty; } } } - - if (m_routeType == SAFEST_ROUTE) - { - // NEO Jank Cheat: Incorporate enemy bot paths so that we don't run directly into their line of fire - // Intended for use by ghost carrier team, to emulate a team that knows where enemies are likely to ambush - // Compensates for bots' lack of meta knowledge by making them prefer routes not reserved by enemies - // Adheres to cheat against bots but not against humans philosophy by not considering human players' positions - cost += CNEOBotPathReservations()->GetPredictedFriendlyPathCount(area->GetID(), GetEnemyTeam(m_me->GetTeamNumber())) * neo_bot_path_reservation_penalty.GetFloat() * 2; - } } - // ------------------------------------------------------------------------------------------------ - if (area->HasAttributes(NAV_MESH_FUNC_COST)) + if (m_routeType == SAFEST_ROUTE) { - cost *= area->ComputeFuncNavCost(m_me); - DebuggerBreakOnNaN_StagingOnly(cost); + // NEO Jank Cheat: Incorporate enemy bot paths so that we don't run directly into their line of fire + // Intended for use by ghost carrier team, to emulate a team that knows where enemies are likely to ambush + // Compensates for bots' lack of meta knowledge by making them prefer routes not reserved by enemies + // Adheres to cheat against bots but not against humans philosophy by not considering human players' positions + cost += CNEOBotPathReservations()->GetPredictedFriendlyPathCount(area->GetID(), GetEnemyTeam(m_me->GetTeamNumber())) * neo_bot_path_reservation_penalty.GetFloat() * 2; } + } + // ------------------------------------------------------------------------------------------------ - return cost + fromArea->GetCostSoFar(); + if (area->HasAttributes(NAV_MESH_FUNC_COST)) + { + cost *= area->ComputeFuncNavCost(m_me); + DebuggerBreakOnNaN_StagingOnly(cost); } + + return cost + fromArea->GetCostSoFar(); } diff --git a/src/game/server/neo/bot/neo_bot_path_reservation.cpp b/src/game/server/neo/bot/neo_bot_path_reservation.cpp index 89f0244d5b..65d08ecb34 100644 --- a/src/game/server/neo/bot/neo_bot_path_reservation.cpp +++ b/src/game/server/neo/bot/neo_bot_path_reservation.cpp @@ -244,14 +244,24 @@ bool CNEOBotPathReservationSystem::IsAreaReservedByTeammate(CNavArea *area, CNEO * Clear all current path reservations. */ void CNEOBotPathReservationSystem::Clear() +{ + ClearRound(); + m_AreaAvoidPenalties.RemoveAll(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Clear round specific path reservations. + */ +void CNEOBotPathReservationSystem::ClearRound() { for (int team = 0; team < TEAM__TOTAL; ++team) { m_Reservations[team].RemoveAll(); m_AreaPathCounts[team].RemoveAll(); + m_HazardAreas[team].RemoveAll(); } m_BotReservedAreas.RemoveAll(); - m_AreaAvoidPenalties.RemoveAll(); } //------------------------------------------------------------------------------------------------- @@ -342,3 +352,192 @@ float CNEOBotPathReservationSystem::GetAreaAvoidPenalty(unsigned int navAreaID) } return 0.0f; } + +//------------------------------------------------------------------------------------------------- +// Functor to propagate deadly hazard to PVS-adjacent areas +struct CNEOFunctorPropagatePVSDeadlyHazard +{ + CNEOFunctorPropagatePVSDeadlyHazard(float expireTime, int teamID) + : m_expireTime(expireTime), m_teamID(teamID) + { + } + + bool operator()(CNavArea *area) + { + CNEOBotPathReservations()->AddDeadlyHazard(area->GetID(), m_expireTime, m_teamID); + return true; + } + + float m_expireTime; + int m_teamID; +}; + +//------------------------------------------------------------------------------------------------- +// Marks an area temporarily for bots to avoid or escape from +void CNEOBotPathReservationSystem::AddDeadlyHazard(int navAreaID, float expireTime, int teamID, bool propagatePVS) +{ + if ( !neo_bot_path_reservation_avoid_penalty_enable.GetBool() ) + { + return; + } + + if ( (teamID < 0) || (teamID >= TEAM__TOTAL) ) + { + return; + } + + int index = m_HazardAreas[teamID].Find(navAreaID); + if ( !m_HazardAreas[teamID].IsValidIndex(index) ) + { + // Initialize blank slate lookup entry + HazardInfo blank; + blank.hazardExpireTime = expireTime; + blank.smokeExpireTime = 0.0f; + index = m_HazardAreas[teamID].Insert(navAreaID, blank); + } + else + { + HazardInfo &existing = m_HazardAreas[teamID][index]; + // Optimizing simplification: assume new time is later to skip comparisons + // May also work for resetting an area to be non-hazardous early + existing.hazardExpireTime = expireTime; + } + + if ( propagatePVS ) + { + CNavArea *area = TheNavMesh->GetNavAreaByID(navAreaID); + if (area) + { + CNEOFunctorPropagatePVSDeadlyHazard propagate(expireTime, teamID); + // CompletelyVisible: fewer areas to iterate through and definitely exposed + // vs PotentiallyVisible: for narrow corridors, some areas would count even if only a sliver was exposed + area->ForAllCompletelyVisibleAreas(propagate); + } + } +} + +//------------------------------------------------------------------------------------------------- +void CNEOBotPathReservationSystem::AddFragHazard(int navAreaID, float expireTime, int teamID) +{ + AddDeadlyHazard(navAreaID, expireTime, teamID, true); + // true (propagatePVS) - propagate hazard to all adjacent PVS areas + // shorthand for labeling entire trajectory and potential stray angles as hazardous + // also intended for grenade thrower to duck behind cover from perspective of target +} + +//------------------------------------------------------------------------------------------------- +// Functor to propagate a smoke hazard to PVS-adjacent areas +struct CNEOFunctorPropagatePVSSmokeHazard +{ + CNEOFunctorPropagatePVSSmokeHazard(float expireTime, int teamID) + : m_expireTime(expireTime), m_teamID(teamID) + { + } + + bool operator()(CNavArea *area) + { + CNEOBotPathReservations()->AddSmokeHazard(area->GetID(), m_expireTime, m_teamID, false); + return true; + } + + float m_expireTime; + int m_teamID; +}; + +//------------------------------------------------------------------------------------------------- +// Marks an area temporarily for bots to avoid or escape from +// Support bots ignore this smoke hazard +void CNEOBotPathReservationSystem::AddSmokeHazard(int navAreaID, float expireTime, int teamID, bool propagatePVS) +{ + if ( !neo_bot_path_reservation_avoid_penalty_enable.GetBool() ) + { + return; + } + + if (teamID < 0 || teamID >= TEAM__TOTAL) + { + return; + } + + int index = m_HazardAreas[teamID].Find(navAreaID); + if (!m_HazardAreas[teamID].IsValidIndex(index)) + { + // Initialize blank slate lookup entry + HazardInfo blank; + blank.hazardExpireTime = 0.0f; + blank.smokeExpireTime = expireTime; + index = m_HazardAreas[teamID].Insert(navAreaID, blank); + } + else + { + HazardInfo &existing = m_HazardAreas[teamID][index]; + // Optimizing simplification: assume new time is later to skip comparisons + // May also work for resetting an area to be non-hazardous early + existing.smokeExpireTime = expireTime; + + } + + if (propagatePVS) + { + CNavArea *area = TheNavMesh->GetNavAreaByID(navAreaID); + if (area) + { + CNEOFunctorPropagatePVSSmokeHazard propagate(expireTime, teamID); + // CompletelyVisible: fewer areas to iterate through and definitely exposed + // vs PotentiallyVisible: for narrow corridors, some areas would count even if only a sliver was exposed + area->ForAllCompletelyVisibleAreas(propagate); + } + } +} + +//------------------------------------------------------------------------------------------------- +float CNEOBotPathReservationSystem::GetAreaHazardousTime(int navAreaID, const CNEOBot *me) const +{ + if (!neo_bot_path_reservation_avoid_penalty_enable.GetBool()) + { + return 0.0f; + } + + if (!me) + { + return 0.0f; + } + + int teamID = me->GetTeamNumber(); + + if (teamID < 0 || teamID >= TEAM__TOTAL) + { + return 0.0f; + } + + int index = m_HazardAreas[teamID].Find(navAreaID); + if (m_HazardAreas[teamID].IsValidIndex(index)) + { + const HazardInfo &h = m_HazardAreas[teamID][index]; + + auto hazardTimeLeft = h.hazardExpireTime - gpGlobals->curtime; + if (hazardTimeLeft > 0) + { + // All bots avoid explosive hazards + return hazardTimeLeft; + } + + // Support class can see through smoke + if (me->GetClass() != NEO_CLASS_SUPPORT) + { + auto smokeTimeLeft = h.smokeExpireTime - gpGlobals->curtime; + if (smokeTimeLeft > 0) + { + return smokeTimeLeft; + } + } + } + + return 0.0f; +} + +//------------------------------------------------------------------------------------------------- +bool CNEOBotPathReservationSystem::IsAreaHazardous(int navAreaID, const CNEOBot *me) const +{ + return GetAreaHazardousTime(navAreaID, me) > 0; +} diff --git a/src/game/server/neo/bot/neo_bot_path_reservation.h b/src/game/server/neo/bot/neo_bot_path_reservation.h index e41401e8b3..932642d1aa 100644 --- a/src/game/server/neo/bot/neo_bot_path_reservation.h +++ b/src/game/server/neo/bot/neo_bot_path_reservation.h @@ -13,6 +13,12 @@ struct ReservationInfo float flExpirationTime; // When the reservation expires (gpGlobals->curtime) }; +struct HazardInfo +{ + float smokeExpireTime; // when the smoke hazard risk expires + float hazardExpireTime; // when a general deadly hazard risk expires +}; + struct BotReservedAreas_t { CUtlVector areas; @@ -48,6 +54,7 @@ class CNEOBotPathReservationSystem { m_Reservations[i].SetLessFunc(ReservationLessFunc); m_AreaPathCounts[i].SetLessFunc(ReservationLessFunc); + m_HazardAreas[i].SetLessFunc(ReservationLessFunc); } } @@ -55,6 +62,7 @@ class CNEOBotPathReservationSystem void ReleaseArea(CNavArea *area, CNEOBot *bot); bool IsAreaReservedByTeammate(CNavArea *area, CNEOBot *avoider) const; void Clear(); + void ClearRound(); void ReleaseAllAreas(CNEOBot *bot); void IncrementPredictedFriendlyPathCount( int areaID, int teamID ); @@ -64,6 +72,12 @@ class CNEOBotPathReservationSystem void IncrementAreaAvoidPenalty(unsigned int navAreaID, float penaltyAmount); float GetAreaAvoidPenalty(unsigned int navAreaID) const; + void AddDeadlyHazard(int navAreaID, float expireTime, int teamID, bool propagatePVS = false); + void AddFragHazard(int navAreaID, float expireTime, int teamID); + void AddSmokeHazard(int navAreaID, float expireTime, int teamID, bool propagatePVS = true); + float GetAreaHazardousTime(int navAreaID, const CNEOBot *me) const; + bool IsAreaHazardous(int navAreaID, const CNEOBot *me) const; + // Allow the global accessor to access private members if needed, though constructor handles init now. friend CNEOBotPathReservationSystem* CNEOBotPathReservations(); @@ -72,6 +86,7 @@ class CNEOBotPathReservationSystem CUtlMap m_BotReservedAreas; CUtlMap m_AreaPathCounts[TEAM__TOTAL]; CUtlMap m_AreaAvoidPenalties; + CUtlMap m_HazardAreas[TEAM__TOTAL]; }; diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 30f9053365..fd95ae1ff3 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -23,6 +23,7 @@ #include "neo_model_manager.h" #include "neo_ghost_spawn_point.h" #include "neo_ghost_cap_point.h" +#include "neo/bot/neo_bot_path_reservation.h" #include "neo/weapons/weapon_ghost.h" #include "neo/weapons/weapon_neobasecombatweapon.h" #include "eventqueue.h" @@ -2823,6 +2824,10 @@ void CNEORules::StartNextRound() m_flNeoNextRoundStartTime = 0; m_flGhostLastHeld = 0; +#ifdef GAME_DLL + CNEOBotPathReservations()->ClearRound(); +#endif + CleanUpMap(); const bool bFromStarting = (m_nRoundStatus == NeoRoundStatus::Warmup || m_nRoundStatus == NeoRoundStatus::Countdown);