From ed7edec065ef3ff4bf7476e6dab1ca39e2f16cf4 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 19:06:37 -0600 Subject: [PATCH 1/2] Duck when only a crouched hull fits ahead CNEOBotLocomotion overrode AdjustPosture with an empty body, carried over from the TF / HL2MP bot template, so bots never crouched to get under something unless the nav area was marked CROUCH. A bot standing under a beam, a light fitting or a lintel it could duck under pushed into it until the stuck monitor fired. AdjustPosture now sweeps a standing hull from step height 32 u towards the move goal. When that hits the world or a prop and a crouched hull does not, the bot holds crouch. Players are ignored, since they move. --- .../server/neo/bot/neo_bot_locomotion.cpp | 63 +++++++++++++++++++ src/game/server/neo/bot/neo_bot_locomotion.h | 5 +- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/game/server/neo/bot/neo_bot_locomotion.cpp b/src/game/server/neo/bot/neo_bot_locomotion.cpp index 0ec6cc339a..071a9d7afd 100644 --- a/src/game/server/neo/bot/neo_bot_locomotion.cpp +++ b/src/game/server/neo/bot/neo_bot_locomotion.cpp @@ -6,6 +6,10 @@ extern ConVar falldamage; +// How far towards its move goal a bot checks its headroom: about a hull width, so it ducks at the +// obstacle rather than a long way before it +static const float NEO_BOT_HEADROOM_LOOKAHEAD = 32.0f; + //----------------------------------------------------------------------------------------- void CNEOBotLocomotion::Update( void ) { @@ -56,6 +60,65 @@ void CNEOBotLocomotion::Approach( const Vector& pos, float goalWeight ) } +//----------------------------------------------------------------------------------------- +// Duck when a standing hull would hit something just ahead but a crouched one would not: a beam, +// light fitting or lintel at head height. The bot template this came from never crouched to navigate +void CNEOBotLocomotion::AdjustPosture( const Vector &moveGoal ) +{ + if ( !IsOnGround() ) + { + return; + } + + CNEOBot *me = ToNEOBot( GetBot()->GetEntity() ); + if ( !me || me->IsBotOnLadder() ) + { + return; + } + + if ( IsOnlyCrouchClearAhead( moveGoal ) ) + { + me->PressCrouchButton( 0.3f ); + } +} + + +//----------------------------------------------------------------------------------------- +bool CNEOBotLocomotion::IsOnlyCrouchClearAhead( const Vector &moveGoal ) const +{ + const Vector &feet = GetFeet(); + Vector toGoal = moveGoal - feet; + toGoal.z = 0.0f; + const float flGoalDist = toGoal.NormalizeInPlace(); + if ( flGoalDist < 1.0f ) + { + return false; + } + + // Sweep from step height up, so a step the bot can walk onto does not count as blocking + const IBody *body = GetBot()->GetBodyInterface(); + const float flStep = GetStepHeight(); + const float flHalfWidth = 0.5f * body->GetHullWidth(); + const Vector mins( -flHalfWidth, -flHalfWidth, 0.0f ); + const Vector standMaxs( flHalfWidth, flHalfWidth, body->GetStandHullHeight() - flStep ); + const Vector crouchMaxs( flHalfWidth, flHalfWidth, body->GetCrouchHullHeight() - flStep ); + const Vector from = feet + Vector( 0.0f, 0.0f, flStep ); + const Vector to = from + toGoal * Min( flGoalDist, NEO_BOT_HEADROOM_LOOKAHEAD ); + + // Players move out of the way, so only the world and props count + NextBotTraceFilterIgnoreActors filter( GetBot()->GetEntity(), COLLISION_GROUP_NONE ); + trace_t trace; + TraceHull( from, to, mins, standMaxs, body->GetSolidMask(), &filter, &trace ); + if ( trace.fraction >= 1.0f && !trace.startsolid ) + { + return false; + } + + TraceHull( from, to, mins, crouchMaxs, body->GetSolidMask(), &filter, &trace ); + return trace.fraction >= 1.0f && !trace.startsolid; +} + + //----------------------------------------------------------------------------------------- // Distance at which we will die if we fall float CNEOBotLocomotion::GetDeathDropHeight( void ) const diff --git a/src/game/server/neo/bot/neo_bot_locomotion.h b/src/game/server/neo/bot/neo_bot_locomotion.h index 427d0e5e45..be7d1aa459 100644 --- a/src/game/server/neo/bot/neo_bot_locomotion.h +++ b/src/game/server/neo/bot/neo_bot_locomotion.h @@ -31,7 +31,10 @@ class CNEOBotLocomotion : public PlayerLocomotion virtual bool IsEntityTraversable( CBaseEntity *obstacle, TraverseWhenType when = EVENTUALLY ) const; protected: - virtual void AdjustPosture( const Vector &moveGoal ) { } // never crouch to navigate + virtual void AdjustPosture( const Vector &moveGoal ); // duck when only a crouched hull fits ahead + +private: + bool IsOnlyCrouchClearAhead( const Vector &moveGoal ) const; }; inline float CNEOBotLocomotion::GetMaxJumpHeight( void ) const From ee8bcf9d42ae4c699078bee210ba75845fd3f29b Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 19:07:37 -0600 Subject: [PATCH 2/2] Check headroom every 0.2 s and skip ladder ends The headroom check swept two hulls on every Approach call, every tick for every moving bot. Run it every 0.2 s instead, looking as far ahead as the bot moves before the next check plus a hull width, and let one crouch press (0.3 s) last until the next check. Skip it in a crouch area, where the bot already holds crouch, and in an area at a ladder's foot or top: bots approaching a ladder ducked under the ladder's own frame and failed to mount. --- .../server/neo/bot/neo_bot_locomotion.cpp | 40 ++++++++++++++++--- src/game/server/neo/bot/neo_bot_locomotion.h | 5 ++- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/game/server/neo/bot/neo_bot_locomotion.cpp b/src/game/server/neo/bot/neo_bot_locomotion.cpp index 071a9d7afd..08a107c364 100644 --- a/src/game/server/neo/bot/neo_bot_locomotion.cpp +++ b/src/game/server/neo/bot/neo_bot_locomotion.cpp @@ -6,9 +6,12 @@ extern ConVar falldamage; -// How far towards its move goal a bot checks its headroom: about a hull width, so it ducks at the -// obstacle rather than a long way before it +// How far towards its move goal a bot checks its headroom: at least about a hull width, so it ducks at the +// obstacle rather than a long way before it, static const float NEO_BOT_HEADROOM_LOOKAHEAD = 32.0f; +// and how often, while the crouch press of one check lasts until the next +static const float NEO_BOT_HEADROOM_CHECK_INTERVAL = 0.2f; +static const float NEO_BOT_HEADROOM_CROUCH_HOLD = 0.3f; //----------------------------------------------------------------------------------------- void CNEOBotLocomotion::Update( void ) @@ -76,15 +79,40 @@ void CNEOBotLocomotion::AdjustPosture( const Vector &moveGoal ) return; } - if ( IsOnlyCrouchClearAhead( moveGoal ) ) + // A crouch area already holds the duck, and a bot at a ladder's foot or top lines up with the ladder standing + const CNavArea *area = me->GetLastKnownArea(); + if ( area && ( area->HasAttributes( NAV_MESH_CROUCH ) || IsLadderEnd( area ) ) ) { - me->PressCrouchButton( 0.3f ); + return; + } + + if ( !m_headroomTimer.IsElapsed() ) + { + return; } + + m_headroomTimer.Start( NEO_BOT_HEADROOM_CHECK_INTERVAL ); + + // look as far as the bot moves before the next check, plus a hull width + const float flReach = MAX( NEO_BOT_HEADROOM_LOOKAHEAD, + GetGroundSpeed() * NEO_BOT_HEADROOM_CHECK_INTERVAL + GetBot()->GetBodyInterface()->GetHullWidth() ); + if ( IsOnlyCrouchClearAhead( moveGoal, flReach ) ) + { + me->PressCrouchButton( NEO_BOT_HEADROOM_CROUCH_HOLD ); + } +} + + +//----------------------------------------------------------------------------------------- +bool CNEOBotLocomotion::IsLadderEnd( const CNavArea *area ) +{ + return area->GetLadders( CNavLadder::LADDER_UP )->Count() > 0 + || area->GetLadders( CNavLadder::LADDER_DOWN )->Count() > 0; } //----------------------------------------------------------------------------------------- -bool CNEOBotLocomotion::IsOnlyCrouchClearAhead( const Vector &moveGoal ) const +bool CNEOBotLocomotion::IsOnlyCrouchClearAhead( const Vector &moveGoal, float flReach ) const { const Vector &feet = GetFeet(); Vector toGoal = moveGoal - feet; @@ -103,7 +131,7 @@ bool CNEOBotLocomotion::IsOnlyCrouchClearAhead( const Vector &moveGoal ) const const Vector standMaxs( flHalfWidth, flHalfWidth, body->GetStandHullHeight() - flStep ); const Vector crouchMaxs( flHalfWidth, flHalfWidth, body->GetCrouchHullHeight() - flStep ); const Vector from = feet + Vector( 0.0f, 0.0f, flStep ); - const Vector to = from + toGoal * Min( flGoalDist, NEO_BOT_HEADROOM_LOOKAHEAD ); + const Vector to = from + toGoal * Min( flGoalDist, flReach ); // Players move out of the way, so only the world and props count NextBotTraceFilterIgnoreActors filter( GetBot()->GetEntity(), COLLISION_GROUP_NONE ); diff --git a/src/game/server/neo/bot/neo_bot_locomotion.h b/src/game/server/neo/bot/neo_bot_locomotion.h index be7d1aa459..fa86811bd3 100644 --- a/src/game/server/neo/bot/neo_bot_locomotion.h +++ b/src/game/server/neo/bot/neo_bot_locomotion.h @@ -34,7 +34,10 @@ class CNEOBotLocomotion : public PlayerLocomotion virtual void AdjustPosture( const Vector &moveGoal ); // duck when only a crouched hull fits ahead private: - bool IsOnlyCrouchClearAhead( const Vector &moveGoal ) const; + bool IsOnlyCrouchClearAhead( const Vector &moveGoal, float flReach ) const; + static bool IsLadderEnd( const CNavArea *area ); + + CountdownTimer m_headroomTimer; }; inline float CNEOBotLocomotion::GetMaxJumpHeight( void ) const