Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions src/game/server/neo/bot/behavior/neo_bot_behavior.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ void CNEOBotMainAction::FireWeaponAtEnemy( CNEOBot *me )
"Aiming at a visible ghoster threat");
}

if ( me->IsCombatWeapon( myWeapon ) )
if ( myWeapon && me->IsCombatWeapon( myWeapon ) )
Copy link
Contributor Author

@sunzenshen sunzenshen Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To get rid of a warning in proximity to WIP changes.

{
if (myWeapon->GetNeoWepBits() & NEO_WEP_BALC)
{
Expand All @@ -783,7 +783,6 @@ void CNEOBotMainAction::FireWeaponAtEnemy( CNEOBot *me )
}
else if (myWeapon->m_iClip1 <= 0)
{
me->EnableCloak(3.0f);
me->PressCrouchButton(0.3f);
if (m_isWaitingForFullReload)
{
Expand All @@ -802,15 +801,9 @@ void CNEOBotMainAction::FireWeaponAtEnemy( CNEOBot *me )
}
return;
}
else if (myWeapon->GetNeoWepBits() & NEO_WEP_SUPPRESSED)
{
me->EnableCloak(3.0f);
}
else
{
// don't waste cloak budget on thermoptic disrupting weapon
me->DisableCloak();
}

// Even if my weapon is unsuppressed, better than nothing
me->EnableCloak(3.0f);
Copy link
Contributor Author

@sunzenshen sunzenshen Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The argument for me->EnableCloak(3.0f); is an parameter to only enable cloak if you have that many seconds left. We could definitely set up different cloak reserve time thresholds for different classes and suppressed/unsuppressed weapons.

RE: https://discord.com/channels/1235346473827434517/1244281729036845200/1449801332528513138


if ( me->IsContinuousFireWeapon( myWeapon ) )
{
Expand Down
2 changes: 1 addition & 1 deletion src/game/server/neo/bot/neo_bot_body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ bool CNEOBotBody::IsCloakEnabled() const
{
// used for determining if bot needs to press thermoptic button
// we are only interested in the toggle state not visibility in this context
// so do not use GetBotPerceivedCloakState() here
// so do not use GetBotCloakStateDisrupted() here
auto me = NEOPlayerEnt();
return me && me->GetInThermOpticCamo();
}
Expand Down
160 changes: 122 additions & 38 deletions src/game/server/neo/neo_player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include "player_resource.h"
#include "neo_player_shared.h"
#include "bot/neo_bot.h"
#include "nav_mesh.h"

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
Expand Down Expand Up @@ -165,6 +166,70 @@ ConVar bot_class("bot_class", "-1", 0, "Force all bots to spawn with the specifi
static void BotChangeClassFn(const CCommand& args);
ConCommand bot_changeclass("bot_changeclass", BotChangeClassFn, "Force all bots to switch to the specified class number.");

// Bot Cloak Detection Thresholds
// Base detection chance ratio (0.0 - 1.0) for bots to notice a cloaked target based on difficulty
// e.g. 0 implies the bot is oblivious to anything, while 1.0 implies a bot that can roll very high on detection checks
ConVar sv_neo_bot_cloak_detection_threshold_ratio_easy("sv_neo_bot_cloak_detection_threshold_ratio_easy", "0.65", FCVAR_NONE, "Bot cloak detection threshold for easy difficulty observers", true, 0.0f, true, 1.0f);
ConVar sv_neo_bot_cloak_detection_threshold_ratio_normal("sv_neo_bot_cloak_detection_threshold_ratio_normal", "0.70", FCVAR_NONE, "Bot cloak detection threshold for normal difficulty observers", true, 0.0f, true, 1.0f);
ConVar sv_neo_bot_cloak_detection_threshold_ratio_hard("sv_neo_bot_cloak_detection_threshold_ratio_hard", "0.75", FCVAR_NONE, "Bot cloak detection threshold for hard difficulty observers", true, 0.0f, true, 1.0f);
ConVar sv_neo_bot_cloak_detection_threshold_ratio_expert("sv_neo_bot_cloak_detection_threshold_ratio_expert", "0.80", FCVAR_NONE, "Bot cloak detection threshold for expert difficulty observers", true, 0.0f, true, 1.0f);

// Bot Cloak Detection Bonus Factors
// Used in CNEO_Player::GetFogObscuredRatio to determine if the bot (me) can detect a cloaked target given circumstances
// Style guide:
// - positive values mean the cloak effect of the target player is more easily detectable by a bot observer
// - ideally all of these values are positive so the bonus is monotonically increasing
// - see CNEO_Player::IsHiddenByFog for base detection rates by difficulting rating of bot observer
ConVar sv_neo_bot_cloak_debug_perceive_always_on("sv_neo_bot_cloak_debug_perceive_always_on", "0", FCVAR_CHEAT,
"Debug: Force bots to perceive all players as having cloaking on all the time", true, 0, true, 1);

ConVar sv_neo_bot_cloak_detection_bonus_disruption_effect("sv_neo_bot_cloak_detection_bonus_disruption_effect", "30", FCVAR_NONE,
"Bot cloak detection bonus for target being surrounded by the blue disruption effect", true, 0, true, 100);

ConVar sv_neo_bot_cloak_detection_bonus_assault_motion_vision("sv_neo_bot_cloak_detection_bonus_assault_motion_vision", "60", FCVAR_NONE,
"Bot cloak detection bonus for assault class detecting movement with motion vision", true, 0, true, 100);

// Support has difficulty seeing cloak in thermal vision
ConVar sv_neo_bot_cloak_detection_bonus_non_support("sv_neo_bot_cloak_detection_bonus_non_support", "1", FCVAR_NONE,
"Bot cloak detection bonus for non-support classes", true, 0, true, 100);

ConVar sv_neo_bot_cloak_detection_bonus_observer_stationary("sv_neo_bot_cloak_detection_bonus_observer_stationary", "2", FCVAR_NONE,
"Bot cloak detection bonus for observer being stationary", true, 0, true, 100);

ConVar sv_neo_bot_cloak_detection_bonus_observer_walking("sv_neo_bot_cloak_detection_bonus_observer_walking", "1", FCVAR_NONE,
"Bot cloak detection bonus for observer walking", true, 0, true, 100);

ConVar sv_neo_bot_cloak_detection_bonus_target_running("sv_neo_bot_cloak_detection_bonus_target_running", "2", FCVAR_NONE,
"Bot cloak detection bonus for target running", true, 0, true, 100);

ConVar sv_neo_bot_cloak_detection_bonus_target_moving("sv_neo_bot_cloak_detection_bonus_target_moving", "1", FCVAR_NONE,
"Bot cloak detection bonus for target moving", true, 0, true, 100);

ConVar sv_neo_bot_cloak_detection_bonus_target_standing("sv_neo_bot_cloak_detection_bonus_target_standing", "1", FCVAR_NONE,
"Bot cloak detection bonus for target standing", true, 0, true, 100);

ConVar sv_neo_bot_cloak_detection_bonus_scope_range("sv_neo_bot_cloak_detection_bonus_scope_range", "1", FCVAR_NONE,
"Bot cloak detection bonus for being in scope range", true, 0, true, 100);

ConVar sv_neo_bot_cloak_detection_bonus_shotgun_range("sv_neo_bot_cloak_detection_bonus_shotgun_range", "5", FCVAR_NONE,
"Bot cloak detection bonus for being in shotgun range", true, 0, true, 100);

ConVar sv_neo_bot_cloak_detection_bonus_melee_range("sv_neo_bot_cloak_detection_bonus_melee_range", "50", FCVAR_NONE,
"Bot cloak detection bonus for being in melee range", true, 0, true, 100);

ConVar sv_neo_bot_cloak_detection_bonus_per_injury("sv_neo_bot_cloak_detection_bonus_per_injury", "1", FCVAR_NONE,
"Bot cloak detection bonus per injury event", true, 0, true, 100);

// TODO: Lighting information is not yet baked into NavAreas, so we would need to implement that for bots to detect based on lighting
// See "FIXMEL4DTOMAINMERGE" for how NavArea::ComputeLighting() is not fully implemented
ConVar sv_neo_bot_cloak_detection_bonus_lighting_enabled("sv_neo_bot_cloak_detection_bonus_lighting_enabled", "0", FCVAR_NONE,
"Enable/Disable bot cloak detection lighting bonus", false, 0, false, 1);

// Depends on sv_neo_bot_cloak_detection_bonus_lighting_enabled
ConVar sv_neo_bot_cloak_detection_bonus_lighting("sv_neo_bot_cloak_detection_bonus_lighting", "0", FCVAR_NONE,
"Bot cloak detection bonus for target being in a well lit area (scaled by light intensity ratio 0.0-1.0)", true, 0, true, 100);


void CNEO_Player::RequestSetClass(int newClass)
{
if (newClass < 0 || newClass >= NEO_CLASS_ENUM_COUNT)
Expand Down Expand Up @@ -491,7 +556,7 @@ CNEO_Player::CNEO_Player()
m_flLastAirborneJumpOkTime = 0;
m_flLastSuperJumpTime = 0;
m_botThermOpticCamoDisruptedTimer.Invalidate();
m_mapPlayerFogCache.SetLessFunc( DefLessFunc(int) );


m_bFirstDeathTick = true;
m_bCorpseSet = false;
Expand Down Expand Up @@ -1215,16 +1280,12 @@ bool CNEO_Player::IsHiddenByFog(CBaseEntity* target) const

// Check visibility cache for this player
int playerIndex = targetPlayer->entindex();
int cacheIndex = m_mapPlayerFogCache.Find(playerIndex);

// Ensure an entry exists for this player in the cache
if (cacheIndex == m_mapPlayerFogCache.InvalidIndex())
if (!IsIndexIntoPlayerArrayValid(playerIndex))
{
cacheIndex = m_mapPlayerFogCache.Insert(playerIndex, CNEO_Player_FogCacheEntry());
Assert(cacheIndex != m_mapPlayerFogCache.InvalidIndex());
return false;
}

CNEO_Player_FogCacheEntry& cacheEntry = m_mapPlayerFogCache.Element(cacheIndex);
CNEO_Player_FogCacheEntry& cacheEntry = m_playerFogCache[playerIndex];

// If cache is fresh (within 200ms human reaction time), use cached boolean result
if (gpGlobals->curtime - cacheEntry.m_flUpdateTime < 0.2f) // 200ms cache window
Expand All @@ -1233,25 +1294,28 @@ bool CNEO_Player::IsHiddenByFog(CBaseEntity* target) const
}
else // Cache is stale or new entry, calculate and update
{
// Bot difficulty attention range
// Bot difficulty perception roll range
// This value acts as the upper bound for the bot's detection "roll" as a proxy for skill.
// Higher range = higher potential rolls = higher likelihood of exceeding the obscuredRatio (detect the target).
float fBotDifficultyRange = 1.0f;
if (IsBot())
{
switch (neo_bot_difficulty.GetInt())
{
case CNEOBot::EASY:
fBotDifficultyRange = 0.85f;
fBotDifficultyRange = sv_neo_bot_cloak_detection_threshold_ratio_easy.GetFloat();
break;
case CNEOBot::NORMAL:
fBotDifficultyRange = 0.90;
fBotDifficultyRange = sv_neo_bot_cloak_detection_threshold_ratio_normal.GetFloat();
break;
case CNEOBot::HARD:
fBotDifficultyRange = 0.95;
fBotDifficultyRange = sv_neo_bot_cloak_detection_threshold_ratio_hard.GetFloat();
break;
case CNEOBot::EXPERT:
//fBotDifficultyRange = 1.0f;
fBotDifficultyRange = sv_neo_bot_cloak_detection_threshold_ratio_expert.GetFloat();
break;
default:
//fBotDifficultyRange = 1.0f;
fBotDifficultyRange = sv_neo_bot_cloak_detection_threshold_ratio_expert.GetFloat();
break;
}
}
Expand Down Expand Up @@ -1288,20 +1352,26 @@ float CNEO_Player::GetFogObscuredRatio(CBaseEntity* target) const
return 0.0f;
}

if (GetTeamNumber() == targetPlayer->GetTeamNumber())
if ( NEORules()->IsTeamplay()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In anticipation of DM for bots being fixed:
#1553

&& (GetTeamNumber() == targetPlayer->GetTeamNumber()) )
{
// Teammates are always labeled with IFF markers
// Teammates are always labeled with IFF markers, unless in free-for-all game modes
return 0.0f;
}

// If target is not cloaked, it's not obscured.
if (!targetPlayer->GetBotPerceivedCloakState())
if (!targetPlayer->GetInThermOpticCamo() && !sv_neo_bot_cloak_debug_perceive_always_on.GetBool())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConVar sv_neo_bot_cloak_debug_perceive_always_on is where we can set up a test where we see if bots would notice each other given certain angles/circumstances/etc. You can usually tell without nb_debug if another player was detected, based on bots returning fire or even turning on cloak.

{
return 0.0f; // Not obscured
}

// From this point on, assume we are counting penalties against thermopic effectiveness
int nSneakPenaltyCount = 0; // # of factors that are making target more visible
// From this point on, assume we are counting bonus points towards observer detection
float flDetectionBonus = 0.0f; // # of factors that are helping the observer detect the target

if (targetPlayer->GetBotCloakStateDisrupted())
{
flDetectionBonus += sv_neo_bot_cloak_detection_bonus_disruption_effect.GetFloat();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloak disruption when firing unsuppressed weapons is now a configurable gradient rather than a full visibility flag.

}

// --- Helper Lambdas for Movement ---
constexpr auto isMoving = [](const CNEO_Player* player, float tolerance = 10.0f) {
Expand All @@ -1319,14 +1389,14 @@ float CNEO_Player::GetFogObscuredRatio(CBaseEntity* target) const
// Assault class motion vision
if (GetClass() == NEO_CLASS_ASSAULT && targetIsMoving)
{
// I have motion vision
return 0.0f; // Not obscured
// I have motion vision, but I don't aways have it turned on
flDetectionBonus += sv_neo_bot_cloak_detection_bonus_assault_motion_vision.GetFloat();
}

if (GetClass() != NEO_CLASS_SUPPORT)
{
// thermal vision penalty against cloak
nSneakPenaltyCount += 5;
// Penalize Support as if using thermal vision against cloak
flDetectionBonus += sv_neo_bot_cloak_detection_bonus_non_support.GetFloat();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this value is too high, Supports can look relatively braindead, so don't make this too exaggerated in my opinion.

}

bool observerIsRunning = isRunning(this); // Observer (me) is running
Expand All @@ -1336,59 +1406,73 @@ float CNEO_Player::GetFogObscuredRatio(CBaseEntity* target) const
if (!observerIsMoving) // is NOT moving
{
// movement is most obvious if observer is stationary
nSneakPenaltyCount += 2;
flDetectionBonus += sv_neo_bot_cloak_detection_bonus_observer_stationary.GetFloat();
}
else if (!observerIsRunning) // is walking, and NOT running
{
// movement is more obvious when the observer is walking rather than running
nSneakPenaltyCount += 1;
flDetectionBonus += sv_neo_bot_cloak_detection_bonus_observer_walking.GetFloat();
}
// if running, is less likely to notice movement

// Target Movement Impact
if (targetIsRunning)
{
nSneakPenaltyCount += 2;
flDetectionBonus += sv_neo_bot_cloak_detection_bonus_target_running.GetFloat();
}
else if (targetIsMoving)
{
nSneakPenaltyCount += 1;
flDetectionBonus += sv_neo_bot_cloak_detection_bonus_target_moving.GetFloat();
}

if (!targetPlayer->IsDucking()) // is standing, and NOT ducking
{
// target is more obvious when standing at full height
nSneakPenaltyCount += 1;
flDetectionBonus += sv_neo_bot_cloak_detection_bonus_target_standing.GetFloat();
}

// Distance Impact
// Ranges based on IsRangeGreaterThan numbers from EquipBestWeaponForThreat
const Vector& myPos = GetAbsOrigin();
float targetDistance = (target->GetAbsOrigin() - myPos).LengthSqr();

constexpr float scopeRangeSq = 1000.0f * 1000.0f; // also matches GetMaxAttackRange
constexpr float scopeRangeSq = 1000.0f * 1000.0f; // also matches GetMaxAttackRange
if (targetDistance < scopeRangeSq)
{
nSneakPenaltyCount += 5;
flDetectionBonus += sv_neo_bot_cloak_detection_bonus_scope_range.GetFloat();
}

constexpr float shotgunRangeSq = 250.0f * 250.0f;
constexpr float shotgunRangeSq = 250.0f * 250.0f;
if (targetDistance < shotgunRangeSq)
{
nSneakPenaltyCount += 10;
flDetectionBonus += sv_neo_bot_cloak_detection_bonus_shotgun_range.GetFloat();
}

constexpr float meleeRangeSq = 50.0f * 50.0f;
constexpr float meleeRangeSq = 50.0f * 50.0f;
if (targetDistance < meleeRangeSq) {
nSneakPenaltyCount += 50;
flDetectionBonus += sv_neo_bot_cloak_detection_bonus_melee_range.GetFloat();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Melee range is about the point where not seeing a cloaked player becomes a little incredulous, but tried to set a value where there's some hesitation from the bots at Hard and Expert difficulty.

}

// Injured Target Impact
nSneakPenaltyCount += targetPlayer->GetBotDetectableBleedingInjuryEvents();
flDetectionBonus += (float)targetPlayer->GetBotDetectableBleedingInjuryEvents() * sv_neo_bot_cloak_detection_bonus_per_injury.GetFloat();

// Lighting Impact
// NEO JANK: See "FIXMEL4DTOMAINMERGE" for why this doesn't have any effect yet.
// TODO: Lighting is not yet baked into NavAreas with our current tooling
if (sv_neo_bot_cloak_detection_bonus_lighting_enabled.GetBool() && TheNavMesh)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though CNavArea::ComputeLighting does not compute lighting at a CNavArea, decided to include this in case we ever get around to implementing that, without having to recompile. Default weight could use some follow up thought though.

RE: https://discord.com/channels/1235346473827434517/1244281729036845200/1449808464250536097

{
CNavArea *area = TheNavMesh->GetNearestNavArea(target->GetAbsOrigin(), true, 500.0f);
if (area)
{
// Until lighting is baked into nav areas, lightIntensity will always return 1
float lightIntensity = area->GetLightIntensity(target->GetAbsOrigin());
flDetectionBonus += lightIntensity * sv_neo_bot_cloak_detection_bonus_lighting.GetFloat();
}
}

float obscuredDenominator = 100; // we might have to tune this based on time interval
Assert(nSneakPenaltyCount < obscuredDenominator); // something is wrong with our model statistically
float obscuredRatio = (obscuredDenominator - nSneakPenaltyCount) / obscuredDenominator;
float obscuredDenominator = 100.0f; // scale from 0-100 percent likelyhood to detect every 200ms

float obscuredRatio = Max(0.0f, obscuredDenominator - flDetectionBonus) / obscuredDenominator;
obscuredRatio = Clamp(obscuredRatio, 0.0f, 1.0f);
return obscuredRatio;
}
Expand Down
5 changes: 2 additions & 3 deletions src/game/server/neo/neo_player.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class INEOPlayerAnimState;
#include "simtimer.h"
#include "soundenvelope.h"
#include "utldict.h"
#include "utlmap.h"
#include "hl2mp_player.h"
#include "in_buttons.h"

Expand Down Expand Up @@ -177,7 +176,7 @@ class CNEO_Player : public CHL2MP_Player

bool GetInThermOpticCamo() const { return m_bInThermOpticCamo; }
// bots can't see anything, so they need an additional timer for cloak disruption events
bool GetBotPerceivedCloakState() const { return m_botThermOpticCamoDisruptedTimer.IsElapsed() && m_bInThermOpticCamo; }
bool GetBotCloakStateDisrupted() const { return !m_botThermOpticCamoDisruptedTimer.IsElapsed(); }
bool GetSpectatorTakeoverPlayerPending() const { return m_bSpectatorTakeoverPlayerPending; }

virtual void StartAutoSprint(void) OVERRIDE;
Expand Down Expand Up @@ -337,7 +336,7 @@ class CNEO_Player : public CHL2MP_Player
bool m_bSpectatorTakeoverPlayerPending{false};

// Cache for GetFogObscuredRatio for each player
mutable CUtlMap<int, CNEO_Player_FogCacheEntry> m_mapPlayerFogCache;
mutable CNEO_Player_FogCacheEntry m_playerFogCache[MAX_PLAYERS_ARRAY_SAFE];

private:
CNEO_Player(const CNEO_Player&);
Expand Down