From 4d86dca450aa4257c914f58ba5e5a5c8f679401b Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 14:39:23 -0700 Subject: [PATCH 01/11] Improve bot ladder climbing reliability --- .../neo/bot/behavior/neo_bot_ladder_climb.cpp | 99 +++++++++++++++++-- .../neo/bot/behavior/neo_bot_ladder_climb.h | 8 ++ .../bot/behavior/neo_bot_tactical_monitor.cpp | 5 +- 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp index 1ea0df3573..86dc7cbdec 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp @@ -1,13 +1,15 @@ #include "cbase.h" #include "bot/behavior/neo_bot_ladder_climb.h" #include "nav_ladder.h" +#include "NextBot/Path/NextBotPathFollow.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" //--------------------------------------------------------------------------------------------- CNEOBotLadderClimb::CNEOBotLadderClimb( const CNavLadder *ladder, bool goingUp ) - : m_ladder( ladder ), m_bGoingUp( goingUp ), m_bHasBeenOnLadder( false ) + : m_ladder( ladder ), m_bGoingUp( goingUp ), m_bHasBeenOnLadder( false ), + m_flLastZ( 0.0f ), m_flHighestZ( -FLT_MAX ) { } @@ -23,11 +25,16 @@ ActionResult CNEOBotLadderClimb::OnStart( CNEOBot *me, Action me->StopLookingAroundForEnemies(); // Timeout based on ladder length - float estimatedClimbTime = m_ladder->m_length / MAX_CLIMB_SPEED + 1.0f; + float estimatedClimbTime = m_ladder->m_length / MAX_CLIMB_SPEED + 2.0f; m_timeoutTimer.Start( estimatedClimbTime ); m_bHasBeenOnLadder = false; + ILocomotion *mover = me->GetLocomotionInterface(); + m_flLastZ = mover->GetFeet().z; + m_flHighestZ = m_flLastZ; + m_stuckTimer.Start( STUCK_CHECK_INTERVAL ); + if ( me->IsDebugging( NEXTBOT_PATH ) ) { DevMsg( "%s: Starting ladder climb (%s), length %.1f\n", @@ -52,9 +59,12 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) IBody *body = me->GetBodyInterface(); // Check if we're on the ladder (MOVETYPE_LADDER or locomotion says so) - bool onLadder = ( me->GetMoveType() == MOVETYPE_LADDER ) || - mover->IsUsingLadder() || - mover->IsAscendingOrDescendingLadder(); + // Also treat being in the air (no ground entity) as "still on ladder" to prevent + // premature exit when the bot briefly pops off at the top of a ladder. + bool onLadder = ( me->GetMoveType() == MOVETYPE_LADDER ) || + mover->IsUsingLadder() || + mover->IsAscendingOrDescendingLadder() || + !mover->GetGround(); if ( onLadder ) { @@ -62,9 +72,8 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) } else if ( m_bHasBeenOnLadder ) { - // We were on the ladder but got knocked off - return to reevaluate situation - // Since ladder approach should use ChangeTo climbing behavior, this Done will return to the state BEFORE ladder approach - return Done( "Knocked off ladder - reevaluating situation" ); + // We were on the ladder and are now firmly on ground - climb is complete + return Done( "Dismounted ladder - on solid ground" ); } // Get current position and target @@ -72,13 +81,83 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) float currentZ = myPos.z; float targetZ = m_bGoingUp ? m_ladder->m_top.z : m_ladder->m_bottom.z; + // Track peak height for progress detection + if ( currentZ > m_flHighestZ ) + { + m_flHighestZ = currentZ; + } + + // Stuck detection: if we haven't made vertical progress, bail out gracefully + if ( m_stuckTimer.IsElapsed() ) + { + float verticalDelta = fabsf( currentZ - m_flLastZ ); + if ( verticalDelta < STUCK_Z_TOLERANCE ) + { + // No vertical progress - if we're close enough to the target, consider it done + float distToTarget = fabsf( currentZ - targetZ ); + if ( distToTarget < mover->GetStepHeight() * 2.0f ) + { + return Done( "Near target height with no vertical progress - considering climb complete" ); + } + + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%s: Ladder climb stuck - no vertical progress (delta %.1f)\n", + me->GetDebugIdentifier(), verticalDelta ); + } + } + m_flLastZ = currentZ; + m_stuckTimer.Start( STUCK_CHECK_INTERVAL ); + } + if ( m_bGoingUp ) { - if ( currentZ >= targetZ - mover->GetStepHeight() ) + body->SetDesiredPosture( IBody::STAND ); + + // Check if we've reached the top + if ( currentZ >= targetZ - mover->GetStepHeight() && mover->GetGround() ) { return Done( "Reached top of ladder" ); } + // Near the top of the ladder: try to push toward the navmesh exit area + // This handles cases where the exit is behind or offset from the ladder normal + float distToTop = targetZ - currentZ; + if ( distToTop <= DISMOUNT_PUSH_DISTANCE && m_bHasBeenOnLadder ) + { + // Try to find the dismount area from the current path + const PathFollower *path = me->GetCurrentPath(); + if ( path && path->IsValid() ) + { + const Path::Segment *goal = path->GetCurrentGoal(); + if ( goal ) + { + // Find the next non-ladder segment (the exit area) + const Path::Segment *exitSeg = goal; + while ( exitSeg && exitSeg->ladder ) + { + exitSeg = path->NextSegment( exitSeg ); + } + + if ( exitSeg && exitSeg->area ) + { + // Push toward the exit area center instead of just the ladder normal + Vector exitPos = exitSeg->area->GetCenter(); + exitPos.z = Max( exitPos.z, currentZ ); + body->AimHeadTowards( exitPos, IBody::MANDATORY, 0.1f, nullptr, "Dismounting ladder toward exit" ); + mover->Approach( exitPos, 9999999.9f ); + + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + NDebugOverlay::Line( myPos, exitPos, 0, 255, 0, true, 0.1f ); + } + + return Continue(); + } + } + } + } + // Climb up: look up and push into ladder Vector goal = myPos + 100.0f * ( -m_ladder->GetNormal() + Vector( 0, 0, 2 ) ); body->AimHeadTowards( goal, IBody::MANDATORY, 0.1f, nullptr, "Climbing ladder" ); @@ -86,7 +165,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) } else { - if ( currentZ <= targetZ + mover->GetStepHeight() ) + if ( currentZ <= targetZ + mover->GetStepHeight() && mover->GetGround() ) { return Done( "Reached bottom of ladder" ); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h index 2793bb7d8a..9cd1825ed0 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h @@ -31,4 +31,12 @@ class CNEOBotLadderClimb : public Action bool m_bGoingUp; CountdownTimer m_timeoutTimer; bool m_bHasBeenOnLadder; + + float m_flLastZ; + float m_flHighestZ; + CountdownTimer m_stuckTimer; + + static constexpr float STUCK_CHECK_INTERVAL = 1.0f; + static constexpr float STUCK_Z_TOLERANCE = 5.0f; + static constexpr float DISMOUNT_PUSH_DISTANCE = 16.0f; // How close to target Z before we start pushing toward exit }; 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 7f9d1a5fa1..c19814786e 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 @@ -432,7 +432,10 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter CNEO_Player* pBotPlayer = ToNEOPlayer( me->GetEntity() ); if ( pBotPlayer && !(pBotPlayer->m_nButtons & (IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT)) ) { - AvoidBumpingFriends( me ); + if ( me->GetMoveType() != MOVETYPE_LADDER && me->GetLocomotionInterface()->GetGround() ) + { + AvoidBumpingFriends( me ); + } } me->UpdateDelayedThreatNotices(); From 4e335db15dddd4d75711bf541f9d847d350186ae Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 15:13:58 -0700 Subject: [PATCH 02/11] check ground state --- .../neo/bot/behavior/neo_bot_ladder_climb.cpp | 28 +++++++++++++++---- .../neo/bot/behavior/neo_bot_ladder_climb.h | 6 ++-- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp index 86dc7cbdec..a2225da56a 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp @@ -9,7 +9,7 @@ //--------------------------------------------------------------------------------------------- CNEOBotLadderClimb::CNEOBotLadderClimb( const CNavLadder *ladder, bool goingUp ) : m_ladder( ladder ), m_bGoingUp( goingUp ), m_bHasBeenOnLadder( false ), - m_flLastZ( 0.0f ), m_flHighestZ( -FLT_MAX ) + m_flLastZ( 0.0f ), m_flHighestZ( -FLT_MAX ), m_bHasLeftGround( false ) { } @@ -33,6 +33,7 @@ ActionResult CNEOBotLadderClimb::OnStart( CNEOBot *me, Action ILocomotion *mover = me->GetLocomotionInterface(); m_flLastZ = mover->GetFeet().z; m_flHighestZ = m_flLastZ; + m_bHasLeftGround = !mover->GetGround(); m_stuckTimer.Start( STUCK_CHECK_INTERVAL ); if ( me->IsDebugging( NEXTBOT_PATH ) ) @@ -58,13 +59,22 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) ILocomotion *mover = me->GetLocomotionInterface(); IBody *body = me->GetBodyInterface(); - // Check if we're on the ladder (MOVETYPE_LADDER or locomotion says so) + // Check if we're on the ground + bool onGround = ( mover->GetGround() != nullptr ); + + // Update onLadder status // Also treat being in the air (no ground entity) as "still on ladder" to prevent // premature exit when the bot briefly pops off at the top of a ladder. + bool inAir = !onGround; + if ( inAir ) + { + m_bHasLeftGround = true; + } + bool onLadder = ( me->GetMoveType() == MOVETYPE_LADDER ) || mover->IsUsingLadder() || mover->IsAscendingOrDescendingLadder() || - !mover->GetGround(); + inAir; if ( onLadder ) { @@ -76,6 +86,14 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) return Done( "Dismounted ladder - on solid ground" ); } + // Exit if we touch ground after having actually left it (e.g. at the bottom or top exit) + // This helps prevent "ladder tunnel vision" where the bot stays attached to the + // ladder state while standing on a ledge. + if ( m_bHasBeenOnLadder && onGround && m_bHasLeftGround ) + { + return Done( "Touched ground after progress - ladder climb finished" ); + } + // Get current position and target const Vector& myPos = mover->GetFeet(); float currentZ = myPos.z; @@ -115,7 +133,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) body->SetDesiredPosture( IBody::STAND ); // Check if we've reached the top - if ( currentZ >= targetZ - mover->GetStepHeight() && mover->GetGround() ) + if ( currentZ >= targetZ - mover->GetStepHeight() && onGround ) { return Done( "Reached top of ladder" ); } @@ -165,7 +183,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) } else { - if ( currentZ <= targetZ + mover->GetStepHeight() && mover->GetGround() ) + if ( currentZ <= targetZ + mover->GetStepHeight() && onGround ) { return Done( "Reached bottom of ladder" ); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h index 9cd1825ed0..35599194e6 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h @@ -27,13 +27,15 @@ class CNEOBotLadderClimb : public Action virtual ActionResult OnResume( CNEOBot *me, Action *interruptingAction ) override; private: - const CNavLadder *m_ladder; bool m_bGoingUp; - CountdownTimer m_timeoutTimer; bool m_bHasBeenOnLadder; + bool m_bHasLeftGround; float m_flLastZ; float m_flHighestZ; + + const CNavLadder *m_ladder; + CountdownTimer m_timeoutTimer; CountdownTimer m_stuckTimer; static constexpr float STUCK_CHECK_INTERVAL = 1.0f; From f0dff0e21f0f5e51722d67b0822f330b3e808118 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 18:28:35 -0700 Subject: [PATCH 03/11] minimal --- .../neo/bot/behavior/neo_bot_ladder_climb.cpp | 115 ++++++++++++++++-- .../neo/bot/behavior/neo_bot_ladder_climb.h | 7 ++ 2 files changed, 112 insertions(+), 10 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp index a2225da56a..2d6570acd3 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp @@ -9,7 +9,8 @@ //--------------------------------------------------------------------------------------------- CNEOBotLadderClimb::CNEOBotLadderClimb( const CNavLadder *ladder, bool goingUp ) : m_ladder( ladder ), m_bGoingUp( goingUp ), m_bHasBeenOnLadder( false ), - m_flLastZ( 0.0f ), m_flHighestZ( -FLT_MAX ), m_bHasLeftGround( false ) + m_flLastZ( 0.0f ), m_flHighestZ( -FLT_MAX ), m_bHasLeftGround( false ), + m_bDismountPhase( false ), m_pExitArea( nullptr ) { } @@ -47,6 +48,36 @@ ActionResult CNEOBotLadderClimb::OnStart( CNEOBot *me, Action return Continue(); } +//--------------------------------------------------------------------------------------------- +void CNEOBotLadderClimb::EnterDismountPhase( CNEOBot *me ) +{ + m_bDismountPhase = true; + m_dismountTimer.Start( DISMOUNT_TIMEOUT ); + + // Try to resolve the exit area from the current path + const PathFollower *path = me->GetCurrentPath(); + if ( path && path->IsValid() ) + { + const Path::Segment *seg = path->GetCurrentGoal(); + // Walk forward past any ladder segments to find the ground exit + while ( seg && seg->ladder ) + { + seg = path->NextSegment( seg ); + } + if ( seg && seg->area ) + { + m_pExitArea = seg->area; + } + } + + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%s: Entering dismount phase (exit area %s)\n", + me->GetDebugIdentifier(), + m_pExitArea ? "found" : "NOT found" ); + } +} + //--------------------------------------------------------------------------------------------- // Implementation based on ladder climbing logic in https://github.com/Dragoteryx/drgbase/ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) @@ -62,6 +93,67 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) // Check if we're on the ground bool onGround = ( mover->GetGround() != nullptr ); + //------------------------------------------------------------ + // Dismount phase: we've left the ladder, walk to exit NavArea + //------------------------------------------------------------ + if ( m_bDismountPhase ) + { + // Done: reached the target NavArea + if ( m_pExitArea && me->GetLastKnownArea() == m_pExitArea ) + { + return Done( "Reached next NavArea after dismount" ); + } + + // Safety timeout + if ( m_dismountTimer.IsElapsed() ) + { + return Done( "Dismount walk timed out" ); + } + + // Build look target toward exit area center with vertical bias preserved + const Vector& myPos = mover->GetFeet(); + if ( m_pExitArea ) + { + Vector exitCenter = m_pExitArea->GetCenter(); + + // Flatten the direction to horizontal, then re-add vertical look bias + Vector dir = exitCenter - myPos; + dir.z = 0.0f; + + Vector lookTarget = myPos + dir; + if ( m_bGoingUp ) + { + lookTarget.z += 64.0f; // Look slightly upward while dismounting up + } + else + { + lookTarget.z -= 64.0f; // Look slightly downward while dismounting down + } + + body->AimHeadTowards( lookTarget, IBody::MANDATORY, 0.1f, nullptr, "Walking to exit area" ); + mover->Approach( exitCenter, 9999999.9f ); + + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + NDebugOverlay::Line( myPos, exitCenter, 0, 255, 255, true, 0.1f ); + } + } + else + { + // No exit area resolved - just push forward along the ladder normal + Vector pushDir = -m_ladder->GetNormal(); + Vector pushTarget = myPos + 100.0f * pushDir; + body->AimHeadTowards( pushTarget, IBody::MANDATORY, 0.1f, nullptr, "Dismount push forward" ); + mover->Approach( pushTarget, 9999999.9f ); + } + + return Continue(); + } + + //------------------------------------------------------------ + // Normal ladder climbing phase + //------------------------------------------------------------ + // Update onLadder status // Also treat being in the air (no ground entity) as "still on ladder" to prevent // premature exit when the bot briefly pops off at the top of a ladder. @@ -82,16 +174,16 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) } else if ( m_bHasBeenOnLadder ) { - // We were on the ladder and are now firmly on ground - climb is complete - return Done( "Dismounted ladder - on solid ground" ); + // We were on the ladder and are now firmly on ground - transition to dismount + EnterDismountPhase( me ); + return Continue(); } - // Exit if we touch ground after having actually left it (e.g. at the bottom or top exit) - // This helps prevent "ladder tunnel vision" where the bot stays attached to the - // ladder state while standing on a ledge. + // Transition to dismount if we touch ground after having left it if ( m_bHasBeenOnLadder && onGround && m_bHasLeftGround ) { - return Done( "Touched ground after progress - ladder climb finished" ); + EnterDismountPhase( me ); + return Continue(); } // Get current position and target @@ -132,10 +224,11 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) { body->SetDesiredPosture( IBody::STAND ); - // Check if we've reached the top + // Check if we've reached the top - transition to dismount instead of exiting if ( currentZ >= targetZ - mover->GetStepHeight() && onGround ) { - return Done( "Reached top of ladder" ); + EnterDismountPhase( me ); + return Continue(); } // Near the top of the ladder: try to push toward the navmesh exit area @@ -183,9 +276,11 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) } else { + // Check if we've reached the bottom - transition to dismount instead of exiting if ( currentZ <= targetZ + mover->GetStepHeight() && onGround ) { - return Done( "Reached bottom of ladder" ); + EnterDismountPhase( me ); + return Continue(); } // Climb down: Stare at bottom of ladder while moving forward to it diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h index 35599194e6..71ad390c76 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h @@ -2,6 +2,7 @@ #include "NextBotBehavior.h" #include "bot/neo_bot.h" +#include "nav_area.h" class CNavLadder; @@ -30,15 +31,21 @@ class CNEOBotLadderClimb : public Action bool m_bGoingUp; bool m_bHasBeenOnLadder; bool m_bHasLeftGround; + bool m_bDismountPhase; float m_flLastZ; float m_flHighestZ; const CNavLadder *m_ladder; + const CNavArea *m_pExitArea; CountdownTimer m_timeoutTimer; CountdownTimer m_stuckTimer; + CountdownTimer m_dismountTimer; + + void EnterDismountPhase( CNEOBot *me ); static constexpr float STUCK_CHECK_INTERVAL = 1.0f; static constexpr float STUCK_Z_TOLERANCE = 5.0f; static constexpr float DISMOUNT_PUSH_DISTANCE = 16.0f; // How close to target Z before we start pushing toward exit + static constexpr float DISMOUNT_TIMEOUT = 3.0f; // Max time to walk toward exit area after leaving ladder }; From 562b63b673755047f000b32434df46326bb1a975 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 18:40:38 -0700 Subject: [PATCH 04/11] sdffsd --- .../neo/bot/behavior/neo_bot_ladder_climb.cpp | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp index 2d6570acd3..0b155ecf1e 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp @@ -154,6 +154,11 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) // Normal ladder climbing phase //------------------------------------------------------------ + // Get current position and target (needed by both status checks and movement logic below) + const Vector& myPos = mover->GetFeet(); + float currentZ = myPos.z; + float targetZ = m_bGoingUp ? m_ladder->m_top.z : m_ladder->m_bottom.z; + // Update onLadder status // Also treat being in the air (no ground entity) as "still on ladder" to prevent // premature exit when the bot briefly pops off at the top of a ladder. @@ -174,23 +179,32 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) } else if ( m_bHasBeenOnLadder ) { - // We were on the ladder and are now firmly on ground - transition to dismount - EnterDismountPhase( me ); - return Continue(); + // We were on the ladder and are now firmly on ground. + // For downward descents, only trigger dismount once we're near the bottom; + // the bot may still be standing on the upper staging platform when it first + // touches the ladder, so guard against a premature transition. + const float bottomZ = m_ladder->m_bottom.z; + const float nearBottomThreshold = mover->GetStepHeight() * 4.0f; + if ( m_bGoingUp || ( myPos.z <= bottomZ + nearBottomThreshold ) ) + { + EnterDismountPhase( me ); + return Continue(); + } } - // Transition to dismount if we touch ground after having left it + // Transition to dismount if we touch ground after having left it. + // Same downward guard: don't fire while still on the upper platform. if ( m_bHasBeenOnLadder && onGround && m_bHasLeftGround ) { - EnterDismountPhase( me ); - return Continue(); + const float bottomZ = m_ladder->m_bottom.z; + const float nearBottomThreshold = mover->GetStepHeight() * 4.0f; + if ( m_bGoingUp || ( myPos.z <= bottomZ + nearBottomThreshold ) ) + { + EnterDismountPhase( me ); + return Continue(); + } } - // Get current position and target - const Vector& myPos = mover->GetFeet(); - float currentZ = myPos.z; - float targetZ = m_bGoingUp ? m_ladder->m_top.z : m_ladder->m_bottom.z; - // Track peak height for progress detection if ( currentZ > m_flHighestZ ) { From 8c5eb4c380afb19bba9274578370db2131ef3508 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 18:49:22 -0700 Subject: [PATCH 05/11] on ladder check --- .../server/neo/bot/behavior/neo_bot_ladder_approach.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp index 24737bf880..344a7ab6ef 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp @@ -22,7 +22,7 @@ ActionResult CNEOBotLadderApproach::OnStart( CNEOBot *me, ActionIsDebugging( NEXTBOT_PATH ) ) { @@ -129,7 +129,11 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float interval else { // Within mount range - check if aligned to start climbing - if ( dot < ALIGN_DOT_THRESHOLD ) + bool onLadder = ( me->GetMoveType() == MOVETYPE_LADDER ) || + mover->IsUsingLadder() || + mover->IsAscendingOrDescendingLadder(); + + if ( onLadder || dot < ALIGN_DOT_THRESHOLD ) { if ( me->IsDebugging( NEXTBOT_PATH ) ) { From 1335e2fce87cc94c973ecd619d7947c405840c50 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 21:17:48 -0700 Subject: [PATCH 06/11] prevent state jitter --- .../neo/bot/behavior/neo_bot_ladder_approach.cpp | 12 ++++++++++++ .../neo/bot/behavior/neo_bot_ladder_approach.h | 2 ++ .../server/neo/bot/behavior/neo_bot_ladder_climb.cpp | 6 ++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp index 344a7ab6ef..b9b5a77b3a 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp @@ -153,3 +153,15 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float interval return Continue(); } + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotLadderApproach::OnSuspend( CNEOBot *me, Action *interruptingAction ) +{ + return Done( "OnSuspend: Cancel out of ladder approach, situation will likely become stale." ); +} + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotLadderApproach::OnResume( CNEOBot *me, Action *interruptingAction ) +{ + return Done( "OnResume: Cancel out of ladder approach, situation is likely stale." ); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.h b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.h index b9c18eabf7..31dd0d4031 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.h @@ -22,6 +22,8 @@ class CNEOBotLadderApproach : public Action virtual ActionResult OnStart( CNEOBot *me, Action *priorAction ) override; virtual ActionResult Update( CNEOBot *me, float interval ) override; + virtual ActionResult OnSuspend( CNEOBot *me, Action *interruptingAction ) override; + virtual ActionResult OnResume( CNEOBot *me, Action *interruptingAction ) override; private: const CNavLadder *m_ladder; diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp index 0b155ecf1e..1e456e9609 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp @@ -320,13 +320,11 @@ void CNEOBotLadderClimb::OnEnd( CNEOBot *me, Action *nextAction ) //--------------------------------------------------------------------------------------------- ActionResult CNEOBotLadderClimb::OnSuspend( CNEOBot *me, Action *interruptingAction ) { - me->StartLookingAroundForEnemies(); - return Continue(); + return Done( "OnSuspend: Cancel out of ladder climb, situation will likely become stale." ); } //--------------------------------------------------------------------------------------------- ActionResult CNEOBotLadderClimb::OnResume( CNEOBot *me, Action *interruptingAction ) { - me->StopLookingAroundForEnemies(); - return Continue(); + return Done( "OnResume: Cancel out of ladder climb, situation is likely stale." ); } From 71c33bccaf1fbb005f518d87b672aaf366b07b37 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 21:32:44 -0700 Subject: [PATCH 07/11] climb --- .../bot/behavior/neo_bot_tactical_monitor.cpp | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) 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 c19814786e..d5f1b544b3 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 @@ -16,6 +16,7 @@ #include "bot/behavior/neo_bot_retreat_to_cover.h" #include "bot/behavior/neo_bot_retreat_from_grenade.h" #include "bot/behavior/neo_bot_ladder_approach.h" +#include "bot/behavior/neo_bot_ladder_climb.h" #include "bot/behavior/neo_bot_pause.h" #if 0 // NEO TODO (Adam) Fix picking up weapons, search for dropped weapons to pick up ammo #include "bot/behavior/neo_bot_get_ammo.h" @@ -326,22 +327,25 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::WatchForLadders( CNEOBot *me ) return Continue(); } - // Already using a ladder via locomotion interface - ILocomotion *mover = me->GetLocomotionInterface(); - if ( mover->IsUsingLadder() || mover->IsAscendingOrDescendingLadder() ) - { - return Continue(); - } - // We're approaching a ladder - check distance const float ladderApproachRange = 60.0f; + bool goingUp = (goal->how == GO_LADDER_UP); Vector ladderPos = (goal->how == GO_LADDER_UP) ? goal->ladder->m_bottom : goal->ladder->m_top; + // Already using a ladder via locomotion interface + ILocomotion *mover = me->GetLocomotionInterface(); + if ( mover->IsUsingLadder() || mover->IsAscendingOrDescendingLadder() ) + { + return SuspendFor( + new CNEOBotLadderClimb( goal->ladder, goingUp ), + goingUp ? "Encountered ladder up" : "Encountered ladder down" + ); + } + if ( me->GetAbsOrigin().DistToSqr( ladderPos ) < Square(ladderApproachRange) ) { - bool goingUp = (goal->how == GO_LADDER_UP); return SuspendFor( new CNEOBotLadderApproach( goal->ladder, goingUp ), goingUp ? "Approaching ladder up" : "Approaching ladder down" @@ -432,10 +436,7 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter CNEO_Player* pBotPlayer = ToNEOPlayer( me->GetEntity() ); if ( pBotPlayer && !(pBotPlayer->m_nButtons & (IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT)) ) { - if ( me->GetMoveType() != MOVETYPE_LADDER && me->GetLocomotionInterface()->GetGround() ) - { - AvoidBumpingFriends( me ); - } + AvoidBumpingFriends( me ); } me->UpdateDelayedThreatNotices(); From eb27cf21bf0f36f7d3a0b59d714283a8e24da4c4 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 21:34:42 -0700 Subject: [PATCH 08/11] afsdfasd --- src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h index 71ad390c76..124ff1ed9a 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.h @@ -46,6 +46,6 @@ class CNEOBotLadderClimb : public Action static constexpr float STUCK_CHECK_INTERVAL = 1.0f; static constexpr float STUCK_Z_TOLERANCE = 5.0f; - static constexpr float DISMOUNT_PUSH_DISTANCE = 16.0f; // How close to target Z before we start pushing toward exit - static constexpr float DISMOUNT_TIMEOUT = 3.0f; // Max time to walk toward exit area after leaving ladder + static constexpr float DISMOUNT_PUSH_DISTANCE = 16.0f; // How close to target Z before we start pushing toward exit + static constexpr float DISMOUNT_TIMEOUT = 3.0f; // Max time to walk toward exit area after leaving ladder }; From 9aa3dbde7c409b32ec8e2d7e929da058d5f80a1e Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 21:40:26 -0700 Subject: [PATCH 09/11] ignore enemies on approach --- .../neo/bot/behavior/neo_bot_ladder_approach.cpp | 15 +++++++++++++++ .../neo/bot/behavior/neo_bot_ladder_approach.h | 1 + .../neo/bot/behavior/neo_bot_ladder_climb.cpp | 1 + 3 files changed, 17 insertions(+) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp index b9b5a77b3a..ae1d8253f9 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp @@ -32,6 +32,9 @@ ActionResult CNEOBotLadderApproach::OnStart( CNEOBot *me, Actionm_length ); } + me->StopLookingAroundForEnemies(); + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); + return Continue(); } @@ -154,6 +157,18 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float interval return Continue(); } +//--------------------------------------------------------------------------------------------- +void CNEOBotLadderApproach::OnEnd( CNEOBot *me, Action *nextAction ) +{ + me->StartLookingAroundForEnemies(); + me->ClearAttribute( CNEOBot::IGNORE_ENEMIES ); + + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%s: Finished ladder approach\n", me->GetDebugIdentifier() ); + } +} + //--------------------------------------------------------------------------------------------- ActionResult CNEOBotLadderApproach::OnSuspend( CNEOBot *me, Action *interruptingAction ) { diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.h b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.h index 31dd0d4031..de17612580 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.h @@ -22,6 +22,7 @@ class CNEOBotLadderApproach : public Action virtual ActionResult OnStart( CNEOBot *me, Action *priorAction ) override; virtual ActionResult Update( CNEOBot *me, float interval ) override; + virtual void OnEnd( CNEOBot *me, Action *nextAction ) override; virtual ActionResult OnSuspend( CNEOBot *me, Action *interruptingAction ) override; virtual ActionResult OnResume( CNEOBot *me, Action *interruptingAction ) override; diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp index 1e456e9609..ce78835e61 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp @@ -24,6 +24,7 @@ ActionResult CNEOBotLadderClimb::OnStart( CNEOBot *me, Action // Ignore enemies while climbing me->StopLookingAroundForEnemies(); + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); // suppress reaction to swap back to firearm // Timeout based on ladder length float estimatedClimbTime = m_ladder->m_length / MAX_CLIMB_SPEED + 2.0f; From d35300519abbc1e6ac9c21e66f7cedeb1406589f Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 21:46:44 -0700 Subject: [PATCH 10/11] mooo --- src/game/server/neo/bot/behavior/neo_bot_attack.h | 3 +++ .../neo/bot/behavior/neo_bot_ladder_approach.cpp | 2 +- .../neo/bot/behavior/neo_bot_ladder_climb.cpp | 13 +++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_attack.h b/src/game/server/neo/bot/behavior/neo_bot_attack.h index a8890d07eb..e330a0b38c 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.h +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.h @@ -1,7 +1,10 @@ #pragma once +#include "NextBotBehavior.h" #include "Path/NextBotChasePath.h" +class CNEOBot; + //------------------------------------------------------------------------------- class CNEOBotAttack : public Action< CNEOBot > diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp index ae1d8253f9..6c39403dd2 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_approach.cpp @@ -47,7 +47,7 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float interval return Done( "Ladder approach timeout" ); } - const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true); if ( threat && threat->IsVisibleRecently() ) { if ( me->IsDebugging( NEXTBOT_PATH ) ) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp index ce78835e61..6b6551f51b 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp @@ -1,4 +1,5 @@ #include "cbase.h" +#include "bot/behavior/neo_bot_attack.h" #include "bot/behavior/neo_bot_ladder_climb.h" #include "nav_ladder.h" #include "NextBot/Path/NextBotPathFollow.h" @@ -88,6 +89,17 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) return Done( "Ladder climb timeout" ); } + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true); + if ( threat && threat->IsVisibleRecently() ) + { + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%s: Threat detected during ladder approach - engaging\n", me->GetDebugIdentifier() ); + } + // ChangeTo: We may move away from ladder when fighting, so don't want to get stuck in ladder climb behavior + return ChangeTo( new CNEOBotAttack, "Interrupting climb to engage enemy" ); + } + ILocomotion *mover = me->GetLocomotionInterface(); IBody *body = me->GetBodyInterface(); @@ -311,6 +323,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) void CNEOBotLadderClimb::OnEnd( CNEOBot *me, Action *nextAction ) { me->StartLookingAroundForEnemies(); + me->ClearAttribute( CNEOBot::IGNORE_ENEMIES ); if ( me->IsDebugging( NEXTBOT_PATH ) ) { From 4bb7689b5e3f51440d0b1677daab2268e189f54c Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 22:22:05 -0700 Subject: [PATCH 11/11] kinda known good state --- .../neo/bot/behavior/neo_bot_ladder_climb.cpp | 38 ++----------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp index 6b6551f51b..5a1785e437 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ladder_climb.cpp @@ -25,7 +25,7 @@ ActionResult CNEOBotLadderClimb::OnStart( CNEOBot *me, Action // Ignore enemies while climbing me->StopLookingAroundForEnemies(); - me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); // suppress reaction to swap back to firearm + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); // Timeout based on ladder length float estimatedClimbTime = m_ladder->m_length / MAX_CLIMB_SPEED + 2.0f; @@ -166,15 +166,10 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) //------------------------------------------------------------ // Normal ladder climbing phase //------------------------------------------------------------ - - // Get current position and target (needed by both status checks and movement logic below) const Vector& myPos = mover->GetFeet(); float currentZ = myPos.z; float targetZ = m_bGoingUp ? m_ladder->m_top.z : m_ladder->m_bottom.z; - // Update onLadder status - // Also treat being in the air (no ground entity) as "still on ladder" to prevent - // premature exit when the bot briefly pops off at the top of a ladder. bool inAir = !onGround; if ( inAir ) { @@ -190,33 +185,6 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) { m_bHasBeenOnLadder = true; } - else if ( m_bHasBeenOnLadder ) - { - // We were on the ladder and are now firmly on ground. - // For downward descents, only trigger dismount once we're near the bottom; - // the bot may still be standing on the upper staging platform when it first - // touches the ladder, so guard against a premature transition. - const float bottomZ = m_ladder->m_bottom.z; - const float nearBottomThreshold = mover->GetStepHeight() * 4.0f; - if ( m_bGoingUp || ( myPos.z <= bottomZ + nearBottomThreshold ) ) - { - EnterDismountPhase( me ); - return Continue(); - } - } - - // Transition to dismount if we touch ground after having left it. - // Same downward guard: don't fire while still on the upper platform. - if ( m_bHasBeenOnLadder && onGround && m_bHasLeftGround ) - { - const float bottomZ = m_ladder->m_bottom.z; - const float nearBottomThreshold = mover->GetStepHeight() * 4.0f; - if ( m_bGoingUp || ( myPos.z <= bottomZ + nearBottomThreshold ) ) - { - EnterDismountPhase( me ); - return Continue(); - } - } // Track peak height for progress detection if ( currentZ > m_flHighestZ ) @@ -234,7 +202,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) float distToTarget = fabsf( currentZ - targetZ ); if ( distToTarget < mover->GetStepHeight() * 2.0f ) { - return Done( "Near target height with no vertical progress - considering climb complete" ); + EnterDismountPhase( me ); } if ( me->IsDebugging( NEXTBOT_PATH ) ) @@ -304,7 +272,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) else { // Check if we've reached the bottom - transition to dismount instead of exiting - if ( currentZ <= targetZ + mover->GetStepHeight() && onGround ) + if ( currentZ <= targetZ + mover->GetStepHeight() ) { EnterDismountPhase( me ); return Continue();