From 8d8df033d5056fc2dfcd498a12ad498d00cea04c Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 13 Sep 2026 16:58:12 -0600 Subject: [PATCH] Bots avoid neo_npc_targetsystem via nav PVS --- .../neo_bot_retreat_from_hazard_area.cpp | 8 ++ .../neo/bot/neo_bot_path_reservation.cpp | 45 +++++++++ .../server/neo/bot/neo_bot_path_reservation.h | 3 + src/game/server/neo/neo_npc_targetsystem.cpp | 93 +++++++++++++++++++ src/game/server/neo/neo_npc_targetsystem.h | 6 ++ 5 files changed, 155 insertions(+) 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 index a4dd3d868f..710f54e027 100644 --- 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 @@ -130,6 +130,14 @@ ActionResult CNEOBotRetreatFromHazardArea::Update(CNEOBot *me, float in CNEOBotPathCompute(me, m_path, m_safeArea->GetCenter(), RETREAT_ROUTE); } + // Possibly don't want to cloak with grenade/smoke hazards yet + // because in such situations either the threat noticed you already + // or because they didn't notice you yet, you shouldn't announce your position + if (CNEOBotPathReservations()->IsAreaNpcTurretHazard(myArea->GetID(), me)) + { + me->EnableCloak(3.0f); + } + m_path.Update(me); return Continue(); } 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 65d08ecb34..85a841d6b8 100644 --- a/src/game/server/neo/bot/neo_bot_path_reservation.cpp +++ b/src/game/server/neo/bot/neo_bot_path_reservation.cpp @@ -393,6 +393,7 @@ void CNEOBotPathReservationSystem::AddDeadlyHazard(int navAreaID, float expireTi HazardInfo blank; blank.hazardExpireTime = expireTime; blank.smokeExpireTime = 0.0f; + blank.npcTurretExpireTime = 0.0f; index = m_HazardAreas[teamID].Insert(navAreaID, blank); } else @@ -466,6 +467,7 @@ void CNEOBotPathReservationSystem::AddSmokeHazard(int navAreaID, float expireTim HazardInfo blank; blank.hazardExpireTime = 0.0f; blank.smokeExpireTime = expireTime; + blank.npcTurretExpireTime = 0.0f; index = m_HazardAreas[teamID].Insert(navAreaID, blank); } else @@ -490,6 +492,49 @@ void CNEOBotPathReservationSystem::AddSmokeHazard(int navAreaID, float expireTim } } +//------------------------------------------------------------------------------------------------- +// Area tagged by neo_npc_targetsystem turret, whose motion vision +// cannot see slow cloaked players, so bots escaping it should cloak +void CNEOBotPathReservationSystem::AddNpcTurretHazard(int navAreaID, float expireTime, int teamID, bool propagatePVS) +{ + AddDeadlyHazard(navAreaID, expireTime, teamID, propagatePVS); + + if ( (teamID < 0) || (teamID >= TEAM__TOTAL) ) + { + return; + } + + int index = m_HazardAreas[teamID].Find(navAreaID); + if ( m_HazardAreas[teamID].IsValidIndex(index) ) + { + // Inform the bots in the specific target area to turn on cloak + m_HazardAreas[teamID][index].npcTurretExpireTime = expireTime; + } +} + +//------------------------------------------------------------------------------------------------- +bool CNEOBotPathReservationSystem::IsAreaNpcTurretHazard(int navAreaID, const CNEOBot *me) const +{ + if ( !neo_bot_path_reservation_avoid_penalty_enable.GetBool() || !me ) + { + return false; + } + + int teamID = me->GetTeamNumber(); + if ( (teamID < 0) || (teamID >= TEAM__TOTAL) ) + { + return false; + } + + int index = m_HazardAreas[teamID].Find(navAreaID); + if ( !m_HazardAreas[teamID].IsValidIndex(index) ) + { + return false; + } + + return m_HazardAreas[teamID][index].npcTurretExpireTime > gpGlobals->curtime; +} + //------------------------------------------------------------------------------------------------- float CNEOBotPathReservationSystem::GetAreaHazardousTime(int navAreaID, const CNEOBot *me) const { 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 932642d1aa..402a3189e8 100644 --- a/src/game/server/neo/bot/neo_bot_path_reservation.h +++ b/src/game/server/neo/bot/neo_bot_path_reservation.h @@ -17,6 +17,7 @@ struct HazardInfo { float smokeExpireTime; // when the smoke hazard risk expires float hazardExpireTime; // when a general deadly hazard risk expires + float npcTurretExpireTime; // when a neo_npc_targetsystem hazard expires }; struct BotReservedAreas_t @@ -75,6 +76,8 @@ class CNEOBotPathReservationSystem 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); + void AddNpcTurretHazard(int navAreaID, float expireTime, int teamID, bool propagatePVS = true); + bool IsAreaNpcTurretHazard(int navAreaID, const CNEOBot *me) const; float GetAreaHazardousTime(int navAreaID, const CNEOBot *me) const; bool IsAreaHazardous(int navAreaID, const CNEOBot *me) const; diff --git a/src/game/server/neo/neo_npc_targetsystem.cpp b/src/game/server/neo/neo_npc_targetsystem.cpp index 0923545928..81fc301471 100644 --- a/src/game/server/neo/neo_npc_targetsystem.cpp +++ b/src/game/server/neo/neo_npc_targetsystem.cpp @@ -1,11 +1,19 @@ #include "neo_npc_targetsystem.h" #include "neo_player.h" #include "ammodef.h" +#include "nav_mesh.h" +#include "NextBotManager.h" +#include "bot/neo_bot_path_reservation.h" #include "tier0/memdbgon.h" #define CLOAKED_VELOCITY_THRESHOLD 32400 // 180 horizontal velocity. Slightly under sprint/wigglerun speed +// Bot hazard timing +static constexpr float BOT_HAZARD_REACTION_TIME = 0.25f; +static constexpr float BOT_HAZARD_INTERVAL = BOT_HAZARD_REACTION_TIME; +static constexpr float BOT_HAZARD_DURATION = 2.0f; + LINK_ENTITY_TO_CLASS(neo_npc_targetsystem, CNEO_NPCTargetSystem); BEGIN_DATADESC(CNEO_NPCTargetSystem) @@ -185,6 +193,7 @@ void CNEO_NPCTargetSystem::Think() { m_OnSpotted.FireOutput(pBestTarget, this); m_bTargetAcquired = true; + m_flNextHazardTime = gpGlobals->curtime + BOT_HAZARD_REACTION_TIME; } if (bMiddleIgnore && !m_bMiddleIgnoreActive) @@ -234,6 +243,8 @@ void CNEO_NPCTargetSystem::Think() m_bMiddleIgnoreActive = false; } + PublishBotHazards(pBestTarget); + // Optional - damage the target if (m_hDamageSource && pBestTarget && (iNewZone == ZONE_MIDDLE)) { @@ -260,6 +271,88 @@ void CNEO_NPCTargetSystem::Think() SetNextThink(gpGlobals->curtime + 0.05f); } +//----------------------------------------------------------------------------- +// Bot hazard publishing - Bots cannot fight this entity. +// Hazards: its own area and all adjacent areas for every team (run-over), +// and its targeted player's area once acquired. +//----------------------------------------------------------------------------- + +void CNEO_NPCTargetSystem::PublishBotHazards(CBasePlayer *pTarget) +{ + if (!TheNavMesh->IsLoaded() || TheNextBots().GetNextBotCount() == 0) + { + return; + } + + if (gpGlobals->curtime < m_flNextHazardTime) + { + return; + } + + m_flNextHazardTime = gpGlobals->curtime + BOT_HAZARD_INTERVAL; + const float flExpireTime = gpGlobals->curtime + BOT_HAZARD_DURATION; + + // A team is targeted if a living member passes the filter; on ntre_rogue_ctg + // the filter swaps on ghost pickup so the carrier's team is left out + bool bTeamTargeted[TEAM__TOTAL] = {}; + for (int i = 1; i <= gpGlobals->maxClients; i++) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex(i); + if (!pPlayer || !pPlayer->IsAlive() || pPlayer->GetTeamNumber() < FIRST_GAME_TEAM) + { + continue; + } + + if (!m_pFilter || m_pFilter->PassesFilter(this, pPlayer)) + { + bTeamTargeted[pPlayer->GetTeamNumber()] = true; + } + } + + // Mark areas PVS to the tank as do not hang out in my view + CNavArea *pOwnArea = TheNavMesh->GetNearestNavArea(GetAbsOrigin()); + if (pOwnArea) + { + for (int iTeam = FIRST_GAME_TEAM; iTeam < TEAM__TOTAL; ++iTeam) + { + CNEOBotPathReservations()->AddDeadlyHazard(pOwnArea->GetID(), flExpireTime, iTeam); + + if (bTeamTargeted[iTeam]) + { + AddVisibleHazard(pOwnArea, iTeam, flExpireTime); + } + } + } + + if (pTarget) + { + CNavArea *pTargetArea = TheNavMesh->GetNearestNavArea(pTarget->GetAbsOrigin()); + if (pTargetArea) + { + // Don't propagate the PVS for the targeted bot location to avoid marking the areas around the corner + AddVisibleHazard(pTargetArea, pTarget->GetTeamNumber(), flExpireTime, false); + } + } +} + +void CNEO_NPCTargetSystem::AddVisibleHazard(CNavArea *pArea, int iTeam, float flExpireTime, bool bPropagatePVS) +{ + const bool bMotionVision = m_bMotionVision; + auto addHazard = [flExpireTime, iTeam, bMotionVision, bPropagatePVS](CNavArea *pVisible) + { + if (bMotionVision) + { + CNEOBotPathReservations()->AddNpcTurretHazard(pVisible->GetID(), flExpireTime, iTeam, bPropagatePVS); + } + else + { + CNEOBotPathReservations()->AddDeadlyHazard(pVisible->GetID(), flExpireTime, iTeam, bPropagatePVS); + } + return true; + }; + addHazard(pArea); +} + bool CNEO_NPCTargetSystem::CanSee(CBaseEntity *pEntity) { if (m_pFilter && m_pFilter->PassesFilter(this, pEntity)) diff --git a/src/game/server/neo/neo_npc_targetsystem.h b/src/game/server/neo/neo_npc_targetsystem.h index 620f6ad597..9b3085c65c 100644 --- a/src/game/server/neo/neo_npc_targetsystem.h +++ b/src/game/server/neo/neo_npc_targetsystem.h @@ -2,6 +2,8 @@ #include "cbase.h" #include "filters.h" +class CNavArea; + class CNEO_NPCTargetSystem : public CBaseEntity { public: @@ -45,6 +47,9 @@ class CNEO_NPCTargetSystem : public CBaseEntity bool CanSee(CBaseEntity *pEntity); private: + void PublishBotHazards(CBasePlayer *pTarget); + void AddVisibleHazard(CNavArea *pArea, int iTeam, float flExpireTime, bool bPropagatePVS = true); + enum TargetZone_e { ZONE_NONE = 0, @@ -58,4 +63,5 @@ class CNEO_NPCTargetSystem : public CBaseEntity bool m_bMiddleIgnoreActive = false; float m_flNextFireTime = 0; CBasePlayer *m_pLastBestTarget = nullptr; + float m_flNextHazardTime = 0; }; \ No newline at end of file