From e84ca946d43c5e794035d6cc16229e215b849a03 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 14:39:23 -0700 Subject: [PATCH 1/3] Enhance bot ladder climbing reliability - Leverage ladder climbing swim controls to decouple climbing with look direction - Movement helpers to help bots align with ladder and dismount --- .../server/NextBot/Player/NextBotPlayer.h | 58 +++ .../server/neo/bot/behavior/neo_bot_attack.h | 3 + .../bot/behavior/neo_bot_ladder_approach.cpp | 135 ++++-- .../bot/behavior/neo_bot_ladder_approach.h | 7 +- .../neo/bot/behavior/neo_bot_ladder_climb.cpp | 442 ++++++++++++++++-- .../neo/bot/behavior/neo_bot_ladder_climb.h | 20 +- .../bot/behavior/neo_bot_tactical_monitor.cpp | 21 +- src/game/server/neo/bot/neo_bot.cpp | 15 + src/game/server/neo/bot/neo_bot.h | 2 + .../server/neo/bot/neo_bot_locomotion.cpp | 8 + 10 files changed, 614 insertions(+), 97 deletions(-) diff --git a/src/game/server/NextBot/Player/NextBotPlayer.h b/src/game/server/NextBot/Player/NextBotPlayer.h index d12df0fe5e..e6a775d499 100644 --- a/src/game/server/NextBot/Player/NextBotPlayer.h +++ b/src/game/server/NextBot/Player/NextBotPlayer.h @@ -136,6 +136,12 @@ class INextBotPlayerInput virtual void ReleaseWalkButton( void ) = 0; #ifdef NEO + virtual void PressMoveUpButton( float duration = -1.0f ) = 0; + virtual void ReleaseMoveUpButton( void ) = 0; + + virtual void PressMoveDownButton( float duration = -1.0f ) = 0; + virtual void ReleaseMoveDownButton( void ) = 0; + // This is just an "alias" to PressWalkButton virtual void PressRunButton( float duration = -1.0f ) = 0; virtual void ReleaseRunButton( void ) = 0; @@ -240,6 +246,12 @@ class NextBotPlayer : public PlayerType, public INextBot, public INextBotPlayerI virtual void ReleaseWalkButton( void ); #ifdef NEO + virtual void PressMoveUpButton( float duration = -1.0f ); + virtual void ReleaseMoveUpButton( void ); + + virtual void PressMoveDownButton( float duration = -1.0f ); + virtual void ReleaseMoveDownButton( void ); + // This is just an "alias" to PressWalkButton virtual void PressRunButton( float duration = -1.0f ); virtual void ReleaseRunButton( void ); @@ -292,6 +304,8 @@ class NextBotPlayer : public PlayerType, public INextBot, public INextBotPlayerI CountdownTimer m_walkButtonTimer; CountdownTimer m_buttonScaleTimer; #ifdef NEO + CountdownTimer m_moveUpButtonTimer; + CountdownTimer m_moveDownButtonTimer; CountdownTimer m_dropButtonTimer; CountdownTimer m_thermopticButtonTimer; CountdownTimer m_leanLeftButtonTimer; @@ -515,6 +529,30 @@ inline void NextBotPlayer< PlayerType >::ReleaseWalkButton( void ) } #ifdef NEO +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressMoveUpButton( float duration ) +{ + m_moveUpButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseMoveUpButton( void ) +{ + m_moveUpButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressMoveDownButton( float duration ) +{ + m_moveDownButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseMoveDownButton( void ) +{ + m_moveDownButtonTimer.Invalidate(); +} + template < typename PlayerType > inline void NextBotPlayer< PlayerType >::PressRunButton( float duration ) { @@ -663,6 +701,8 @@ inline void NextBotPlayer< PlayerType >::Spawn( void ) m_forwardScale = m_rightScale = 0.04; m_burningTimer.Invalidate(); #ifdef NEO + m_moveUpButtonTimer.Invalidate(); + m_moveDownButtonTimer.Invalidate(); m_dropButtonTimer.Invalidate(); m_thermopticButtonTimer.Invalidate(); m_leanLeftButtonTimer.Invalidate(); @@ -841,7 +881,25 @@ inline void NextBotPlayer< PlayerType >::PhysicsSimulate( void ) float forwardSpeed = 0.0f; float strafeSpeed = 0.0f; +#ifdef NEO + float verticalSpeed = 0.0f; + + if ( !m_moveUpButtonTimer.IsElapsed() ) + { + verticalSpeed = mover->GetRunSpeed(); + } + else if ( !m_moveDownButtonTimer.IsElapsed() ) + { + verticalSpeed = -mover->GetRunSpeed(); + } + + if ( m_inputButtons & IN_JUMP ) + { + verticalSpeed = mover->GetRunSpeed(); + } +#else float verticalSpeed = ( m_inputButtons & IN_JUMP ) ? mover->GetRunSpeed() : 0.0f; +#endif if ( inputButtons & IN_FORWARD ) { 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 24737bf880..27d9c05ee2 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 @@ -11,6 +11,7 @@ CNEOBotLadderApproach::CNEOBotLadderApproach( const CNavLadder *ladder, bool goingUp ) : m_ladder( ladder ), m_bGoingUp( goingUp ) { + m_ladderCenter = ladder ? ( ladder->m_top + ladder->m_bottom ) * 0.5f : vec3_origin; } //--------------------------------------------------------------------------------------------- @@ -32,20 +33,30 @@ ActionResult CNEOBotLadderApproach::OnStart( CNEOBot *me, Actionm_length ); } + // Don't interfere with look direction + // Can exit out of behavior if threat is present + me->StopLookingAroundForEnemies(); + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); + return Continue(); } //--------------------------------------------------------------------------------------------- // Implementation based on ladder climbing implementation in https://github.com/Dragoteryx/drgbase/ -ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float interval ) +ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float ) { + if ( !m_ladder ) + { + return Done( "Ladder is invalid" ); + } + if ( m_timeoutTimer.IsElapsed() ) { return Done( "Ladder approach timeout" ); } - const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); - if ( threat && threat->IsVisibleRecently() ) + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true); + if ( threat ) { if ( me->IsDebugging( NEXTBOT_PATH ) ) { @@ -60,8 +71,20 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float interval const Vector& myPos = mover->GetFeet(); + // If we end up closer to exit point than entry point, + // may not need to continue climb, exit to reevaluate situation + Vector entryPos = m_bGoingUp ? m_ladder->m_bottom : m_ladder->m_top; + Vector exitPos = m_bGoingUp ? m_ladder->m_top : m_ladder->m_bottom; + float distToEntrySq = myPos.DistToSqr( entryPos ); + float distToExitSq = myPos.DistToSqr( exitPos ); + + if ( distToExitSq < distToEntrySq ) + { + return Done( "Closer to ladder exit than entry, assuming goal reached accidentally" ); + } + // Are we climbing up or down the ladder? - const Vector& targetPos = m_bGoingUp ? m_ladder->m_bottom : m_ladder->m_top; + Vector targetPos = m_bGoingUp ? m_ladder->m_bottom : m_ladder->m_top; // Calculate 2D vector from bot to ladder mount point Vector2D to = ( targetPos - myPos ).AsVector2D(); @@ -71,81 +94,97 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float interval Vector2D ladderNormal2D = m_ladder->GetNormal().AsVector2D(); float dot = DotProduct2D( ladderNormal2D, to ); + // Aim at the ladder center at eye level to carefully attach to ladder + // Eye level in order to not accidentally move and detach between behavior transition + // If bot was looking up or down, sometimes the fast climb movement causes a detachment + Vector lookTarget = m_ladderCenter; + lookTarget.z = me->EyePosition().z; + body->AimHeadTowards( lookTarget, IBody::MANDATORY, 0.1f, nullptr, "Stare at ladder center" ); + if ( me->IsDebugging( NEXTBOT_PATH ) ) { + NDebugOverlay::Cross3D( targetPos, 5.0f, 255, 255, 0, true, 0.1f ); NDebugOverlay::Line( myPos, targetPos, 255, 255, 0, true, 0.1f ); } // Are we aligned and close enough to mount the ladder? - if ( range >= ALIGN_RANGE ) - { - // Far from ladder - just approach the target position - body->AimHeadTowards( targetPos, IBody::CRITICAL, 0.5f, nullptr, "Moving toward ladder" ); - mover->Approach( targetPos ); - } - else if ( range >= MOUNT_RANGE ) + if ( range >= MOUNT_RANGE ) { - // Within alignment range but not mount range - need to align with ladder - if ( dot >= ALIGN_DOT_THRESHOLD ) + // Perpendicular alignment line + Vector2D alignNormal = ladderNormal2D; + if ( dot > 0.0f ) { - // Not aligned - rotate around the ladder to get aligned - Vector2D myPerp( -to.y, to.x ); - Vector2D ladderPerp2D( -ladderNormal2D.y, ladderNormal2D.x ); - - Vector goal = targetPos; - - // Calculate offset to circle around - float alignRange = MOUNT_RANGE + (1.0f + dot) * (ALIGN_RANGE - MOUNT_RANGE); - goal.x -= alignRange * to.x; - goal.y -= alignRange * to.y; - - // Choose direction to circle based on perpendicular alignment - if ( DotProduct2D( to, ladderPerp2D ) < 0.0f ) - { - goal.x += 10.0f * myPerp.x; - goal.y += 10.0f * myPerp.y; - } - else - { - goal.x -= 10.0f * myPerp.x; - goal.y -= 10.0f * myPerp.y; - } + alignNormal = -alignNormal; // Target behind ladder + } - body->AimHeadTowards( goal, IBody::CRITICAL, 0.3f, nullptr, "Aligning with ladder" ); - mover->Approach( goal ); + Vector goal = targetPos; + + // Pull the goal point outwards along the ladder's normal + // to guide bot movement along approach + float offsetDist = Clamp( range * 0.8f, 10.0f, ALIGN_RANGE ); + goal.x += alignNormal.x * offsetDist; + goal.y += alignNormal.y * offsetDist; - if ( me->IsDebugging( NEXTBOT_PATH ) ) - { - NDebugOverlay::Cross3D( goal, 5.0f, 255, 0, 255, true, 0.1f ); - } - } - else + mover->Approach( goal ); + + if ( me->IsDebugging( NEXTBOT_PATH ) ) { - // Aligned - approach the ladder base directly - body->AimHeadTowards( targetPos, IBody::CRITICAL, 0.3f, nullptr, "Approaching ladder" ); - mover->Approach( targetPos ); + NDebugOverlay::Cross3D( goal, 5.0f, 255, 0, 255, true, 0.1f ); } } else { // Within mount range - check if aligned to start climbing - if ( dot < ALIGN_DOT_THRESHOLD ) + bool onLadder = me->IsOnLadder(); + if ( onLadder ) { if ( me->IsDebugging( NEXTBOT_PATH ) ) { DevMsg( "%s: Starting ladder climb\n", me->GetDebugIdentifier() ); } + // Stop the bot before behavior transition to prevent falling off the ladder + // there can be a delay in the state change, so momentum can cause a fall + me->SetAbsVelocity( vec3_origin ); // ChangeTo: if something goes wrong during climb, reevaluate situation return ChangeTo( new CNEOBotLadderClimb( m_ladder, m_bGoingUp ), "Mounting ladder" ); } + else if ( !m_bGoingUp || dot < ALIGN_DOT_THRESHOLD ) + { + // Aligned (or going down), push forward to attach to the ladder + me->PressForwardButton(); + mover->Approach( targetPos ); + } else { // Close but not aligned - continue approaching to align - body->AimHeadTowards( targetPos, IBody::CRITICAL, 0.3f, nullptr, "Approaching ladder" ); mover->Approach( targetPos ); } } 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 ) +{ + 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..aa52154b73 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,13 +22,18 @@ 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; + + static constexpr float ALIGN_RANGE = 100.0f; // Distance to start alignment behavior private: const CNavLadder *m_ladder; bool m_bGoingUp; + Vector m_ladderCenter; CountdownTimer m_timeoutTimer; static constexpr float MOUNT_RANGE = 25.0f; // Distance to start climbing - static constexpr float ALIGN_RANGE = 50.0f; // Distance to start alignment behavior static constexpr float ALIGN_DOT_THRESHOLD = -0.9f; // cos(~25 degrees) alignment tolerance }; 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..b1a4714e9c 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,14 +1,19 @@ #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" // 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_flLastZ( 0.0f ), + m_bDismountPhase( false ), m_bJumpedOffLadder( false ), m_pExitArea( nullptr ) { + m_exitAreaCenter = vec3_origin; + m_ladderForward = ladder ? -ladder->GetNormal() : vec3_origin; } //--------------------------------------------------------------------------------------------- @@ -19,14 +24,73 @@ ActionResult CNEOBotLadderClimb::OnStart( CNEOBot *me, Action return Done( "No ladder specified" ); } - // Ignore enemies while climbing + // Don't interfere with look direction + // Can exit out of behavior if threat is present me->StopLookingAroundForEnemies(); + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); // 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 + 3.0f; m_timeoutTimer.Start( estimatedClimbTime ); - m_bHasBeenOnLadder = false; + ILocomotion *mover = me->GetLocomotionInterface(); + m_flLastZ = mover->GetFeet().z; + m_stuckTimer.Start( STUCK_CHECK_INTERVAL ); + + if ( m_bGoingUp ) + { + // Hull trace check: Ensure clear path to climb in the intended direction. + Vector traceStart = mover->GetFeet(); + + // Trace upwards from feet to check the space the bot would occupy. + // e.g. ladder going through opening in ceiling + float targetZ = m_ladder->m_top.z; + Vector traceEnd = traceStart; + traceEnd.z = MIN( targetZ, traceStart.z + 100.0f ); + + trace_t tr; + UTIL_TraceHull( traceStart, traceEnd, me->WorldAlignMins(), me->WorldAlignMaxs(), MASK_NPCSOLID, me, COLLISION_GROUP_NONE, &tr ); + + // If vertical path blocked by world geometry due to failed approach alignment, + // teleport bot to ideal position, assuming approach placed bot close enough + // Ideally we only transition into this behavior when bot has attached to ladder + if ( tr.DidHit() && tr.fraction < 1.0f ) + { + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%s: Ladder climb path obstructed (fraction %.2f), teleporting to ideal position\n", + me->GetDebugIdentifier(), tr.fraction ); + NDebugOverlay::Box( traceStart, me->WorldAlignMins(), me->WorldAlignMaxs(), 255, 0, 0, 0, 2.0f ); + NDebugOverlay::Box( traceEnd, me->WorldAlignMins(), me->WorldAlignMaxs(), 255, 0, 0, 0, 2.0f ); + } + + // Fallback: Teleport to a center position on the ladder. + mover->Reset(); // clear velocity cache in locomotion interface + me->SetAbsVelocity( vec3_origin ); + + Vector idealPos = m_ladder->GetPosAtHeight( m_flLastZ ); + // Offset slightly from the ladder surface based on the bot's collision box + float offsetDist = me->CollisionProp()->OBBSize().x / 2.0f + 2.0f; + idealPos += m_ladder->GetNormal() * offsetDist; + idealPos.z = m_flLastZ; + + // Face perpendicularly straight on to the ladder (-normal) + Vector idealLookDir = -m_ladder->GetNormal(); + QAngle idealAngles; + VectorAngles( idealLookDir, idealAngles ); + + // Teleport the bot + me->SetAbsOrigin( idealPos ); + me->SetAbsAngles( idealAngles ); + + // Update mover feet to new teleported position for stuck checking + m_flLastZ = idealPos.z; + } + else if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + NDebugOverlay::Line( traceStart, traceEnd, 0, 255, 0, true, 2.0f ); + } + } if ( me->IsDebugging( NEXTBOT_PATH ) ) { @@ -36,74 +100,378 @@ ActionResult CNEOBotLadderClimb::OnStart( CNEOBot *me, Action m_ladder->m_length ); } + // Try to resolve the exit area from the current path early + ResolveExitArea( me ); + return Continue(); } +//--------------------------------------------------------------------------------------------- +void CNEOBotLadderClimb::ResolveExitArea( CNEOBot *me ) +{ + const PathFollower *path = me->GetCurrentPath(); + if ( path && path->IsValid() ) + { + const Path::Segment *seg = path->GetCurrentGoal(); + if ( !seg ) + { + return; + } + + int safetyCounter = 0; + while ( seg && safetyCounter < 100 ) + { + if ( !seg->ladder ) + { + break; + } + + seg = path->NextSegment( seg ); + safetyCounter++; + } + + if ( seg && seg->area ) + { + m_pExitArea = seg->area; + m_exitAreaCenter = m_pExitArea->GetCenter(); + } + } +} + + //--------------------------------------------------------------------------------------------- // Implementation based on ladder climbing logic in https://github.com/Dragoteryx/drgbase/ -ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float interval ) +ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval*/ ) { if ( m_timeoutTimer.IsElapsed() ) { return Done( "Ladder climb timeout" ); } + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true); + if ( threat ) + { + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%s: Threat detected during ladder climb - engaging\n", me->GetDebugIdentifier() ); + } + // Detach from ladder to ready weapon + me->PressJumpButton(); + me->PressBackwardButton(0.1f); + // ChangeTo: We may move away from ladder when fighting, reevaluate later + return ChangeTo( new CNEOBotAttack, "Interrupting climb to engage enemy" ); + } + ILocomotion *mover = me->GetLocomotionInterface(); IBody *body = me->GetBodyInterface(); + const Vector& myPos = mover->GetFeet(); + bool bExitIsBehind = false; + Vector toExit = vec3_origin; + + if ( m_pExitArea ) + { + toExit = m_exitAreaCenter - myPos; + toExit.z = 0.0f; + if ( toExit.Length2DSqr() > 0.01f ) + { + toExit.NormalizeInPlace(); + } + else + { + toExit = m_ladderForward; // Fallback direction + } + + if ( DotProduct( toExit, m_ladderForward ) < 0.0f ) + { + bExitIsBehind = true; + } + } // Check if we're on the ladder (MOVETYPE_LADDER or locomotion says so) - bool onLadder = ( me->GetMoveType() == MOVETYPE_LADDER ) || - mover->IsUsingLadder() || - mover->IsAscendingOrDescendingLadder(); + bool onLadder = me->IsOnLadder(); - if ( onLadder ) + if ( !onLadder ) { - m_bHasBeenOnLadder = true; + if ( mover->IsOnGround() ) + { + return Done( "Reached the ground, ending climb" ); + } + + if ( !m_bDismountPhase && !m_bJumpedOffLadder ) + { + Vector ladderClosestPoint; + CalcClosestPointOnLineSegment( myPos, m_ladder->m_bottom, m_ladder->m_top, ladderClosestPoint ); + if ( myPos.DistToSqr( ladderClosestPoint ) > Square( 100.0f ) ) + { + return Done( "Fallen too far from ladder, resetting" ); + } + } } - else if ( m_bHasBeenOnLadder ) + + //------------------------------------------------------------ + // Ladder climbing phase + //------------------------------------------------------------ + if ( !m_bDismountPhase ) { - // 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" ); - } + float currentZ = myPos.z; + float targetZ = m_bGoingUp ? m_ladder->m_top.z : m_ladder->m_bottom.z; - // 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; + // 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 ) + { + EnterDismountPhase( me ); + } + else + { + // We are stuck mid-climb. Reset scenario by jumping backwards and ending condition + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%s: Ladder climb stuck - jumping backwards to reset (delta %.1f)\n", + me->GetDebugIdentifier(), verticalDelta ); + } + me->PressJumpButton(); + me->PressBackwardButton(0.1f); + return Done( "Got stuck on something climbing the ladder, jumping off to reset." ); + } + } + m_flLastZ = currentZ; + m_stuckTimer.Start( STUCK_CHECK_INTERVAL ); + } - if ( m_bGoingUp ) - { - if ( currentZ >= targetZ - mover->GetStepHeight() ) + // Early jump-off + if ( m_pExitArea ) { - return Done( "Reached top of ladder" ); + float zDistToExit = currentZ - m_exitAreaCenter.z; + + if ( zDistToExit > 0.0f && zDistToExit <= SAFE_FALL_DIST ) + { + EnterDismountPhase( me ); + 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" ); - mover->Approach( goal, 9999999.9f ); + if ( m_bGoingUp ) + { + body->SetDesiredPosture( IBody::STAND ); + + float dismountZ = targetZ; + + if ( m_pExitArea && bExitIsBehind ) + { + dismountZ = MIN( m_exitAreaCenter.z, targetZ ); + } + + if ( currentZ >= dismountZ ) + { + EnterDismountPhase( me ); + return Continue(); + } + + bool bIsClimbingDown = false; + if ( m_pExitArea ) + { + if ( currentZ < m_exitAreaCenter.z ) + { + me->PressMoveUpButton(); + } + else + { + me->PressMoveDownButton(); + bIsClimbingDown = true; + } + } + else + { + me->PressMoveUpButton(); + } + + if ( !bIsClimbingDown ) + { + me->PressForwardButton(); + } + + // Look at the dismount height, slightly behind the ladder + Vector lookTarget = m_ladder->GetPosAtHeight( dismountZ ); + lookTarget -= m_ladder->GetNormal() * 50.0f; + body->AimHeadTowards( lookTarget, IBody::MANDATORY, 0.1f, nullptr, "Climbing up (looking at dismount position)" ); + } + else + { + float dismountZ = targetZ; + + if ( m_pExitArea ) + { + // Allow early drop-off at intermediate floors + dismountZ = MAX( m_exitAreaCenter.z, targetZ ); + } + + // Check if we've reached the bottom or intermediate exit height + if ( currentZ <= dismountZ ) + { + EnterDismountPhase( me ); + return Continue(); + } + + // Adjust vertical height based on target exit area + bool bIsClimbingDown = false; + if ( m_pExitArea && onLadder ) + { + if ( currentZ < m_exitAreaCenter.z ) + { + me->PressMoveUpButton(); + } + else + { + me->PressMoveDownButton(); + bIsClimbingDown = true; + } + } + else if ( onLadder ) + { + me->PressMoveDownButton(); + bIsClimbingDown = true; + } + + // Sanity check: don't press forward if we are actively climbing down + if ( !bIsClimbingDown ) + { + me->PressForwardButton(); + } + + // Look at the dismount height, slightly behind the ladder + Vector lookTarget = m_ladder->GetPosAtHeight( dismountZ ); + lookTarget -= m_ladder->GetNormal() * 50.0f; + body->AimHeadTowards( lookTarget, IBody::MANDATORY, 0.1f, nullptr, "Climbing down (looking at dismount position)" ); + } } - else + + //------------------------------------------------------------ + // Ladder dismount phase + //------------------------------------------------------------ + if ( m_bDismountPhase ) { - if ( currentZ <= targetZ + mover->GetStepHeight() ) + // Reached the target NavArea after the ladder + if ( m_pExitArea && me->GetLastKnownArea() == m_pExitArea ) { - return Done( "Reached bottom of ladder" ); + return Done( "Reached next NavArea after dismount" ); } - // Climb down: Stare at bottom of ladder while moving forward to it - Vector goal = m_ladder->m_bottom; - body->AimHeadTowards( goal, IBody::MANDATORY, 0.1f, nullptr, "Descending ladder fast" ); - mover->Approach( goal, 9999999.9f ); + // Safety timeout + if ( m_dismountTimer.IsElapsed() ) + { + return Done( "Dismount walk timed out" ); + } + + // Build look target toward exit area center with vertical bias preserved + if ( m_pExitArea ) + { + Vector dir = m_exitAreaCenter - myPos; + Vector lookTarget = myPos + dir; + + // Maintain Z-height while on the ladder in the dismount phase + if ( onLadder ) + { + if ( myPos.z < m_exitAreaCenter.z ) + { + me->PressMoveUpButton(); + } + else + { + me->PressMoveDownButton(); + } + } + + body->AimHeadTowards( lookTarget, IBody::MANDATORY, 0.1f, nullptr, "Walking to exit area" ); + + // Jump to detach from ladder if exit is not straight ahead, or if we have reached the exit height + float dot = DotProduct( toExit, m_ladderForward ); + bool bDroppingEarly = ( myPos.z >= m_exitAreaCenter.z ); + + if ( dot < 0.5f || bDroppingEarly ) + { + // Velocity kick to simulate a jump to the next NavArea + if ( !m_bJumpedOffLadder ) + { + me->PressJumpButton(); // mostly to trigger animation if possible + + // Zero Z velocity to avoid additive momentum from climbing, reducing overshoots + Vector currentVel = me->GetAbsVelocity(); + currentVel.z = 0.0f; + mover->Reset(); // clear velocity cache in locomotion interface + me->SetAbsVelocity( currentVel ); + + Vector jumpVelocity = toExit * 150.0f; + jumpVelocity.z = 150.0f; + + me->ApplyAbsVelocityImpulse( jumpVelocity ); + m_bJumpedOffLadder = true; + } + else if ( !onLadder ) + { + me->PressForwardButton(); + } + } + else + { + // Forward exit, just clamber over + me->PressForwardButton(); + } + + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + NDebugOverlay::Line( myPos, m_exitAreaCenter, 0, 255, 255, true, 0.1f ); + } + } + else + { + // No exit area resolved - just push forward along the ladder normal + Vector pushDir = m_ladderForward; + Vector pushTarget = myPos + 100.0f * pushDir; + body->AimHeadTowards( pushTarget, IBody::MANDATORY, 0.1f, nullptr, "Dismount push forward" ); + me->PressForwardButton(); + + if ( mover->IsOnGround() ) + { + return Done( "Dismounted to ground" ); + } + } } return Continue(); } +//--------------------------------------------------------------------------------------------- +void CNEOBotLadderClimb::EnterDismountPhase( CNEOBot *me ) +{ + me->ReleaseMoveUpButton(); + me->ReleaseMoveDownButton(); + me->ReleaseForwardButton(); + me->GetLocomotionInterface()->Reset(); // clear velocity cache in locomotion interface + me->SetAbsVelocity( vec3_origin ); // stop momentum + m_bDismountPhase = true; + m_dismountTimer.Start( DISMOUNT_TIMEOUT ); + ResolveExitArea( me ); + + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%s: Entering dismount phase (exit area %s)\n", + me->GetDebugIdentifier(), + m_pExitArea ? "found" : "NOT found" ); + } +} + //--------------------------------------------------------------------------------------------- void CNEOBotLadderClimb::OnEnd( CNEOBot *me, Action *nextAction ) { me->StartLookingAroundForEnemies(); + me->ClearAttribute( CNEOBot::IGNORE_ENEMIES ); if ( me->IsDebugging( NEXTBOT_PATH ) ) { @@ -114,13 +482,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." ); } 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..7c14e4e65b 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,8 +27,26 @@ class CNEOBotLadderClimb : public Action virtual ActionResult OnResume( CNEOBot *me, Action *interruptingAction ) override; private: + void EnterDismountPhase( CNEOBot *me ); + void ResolveExitArea( CNEOBot *me ); + const CNavLadder *m_ladder; + const CNavArea *m_pExitArea; + bool m_bGoingUp; + bool m_bDismountPhase; + bool m_bJumpedOffLadder; + + float m_flLastZ; + Vector m_exitAreaCenter; + Vector m_ladderForward; + CountdownTimer m_timeoutTimer; - bool m_bHasBeenOnLadder; + CountdownTimer m_stuckTimer; + CountdownTimer m_dismountTimer; + + static constexpr float STUCK_CHECK_INTERVAL = 0.4f; + static constexpr float STUCK_Z_TOLERANCE = 2.0f; + static constexpr float DISMOUNT_TIMEOUT = 3.0f; // Max time to walk toward exit area after leaving ladder + static constexpr float SAFE_FALL_DIST = 200.0f; // Max height to safely drop off a ladder }; 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 a1229e7f1d..c7bb8a9aa6 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,24 @@ 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; + const float ladderApproachRange = CNEOBotLadderApproach::ALIGN_RANGE; + bool goingUp = (goal->how == GO_LADDER_UP); Vector ladderPos = (goal->how == GO_LADDER_UP) ? goal->ladder->m_bottom : goal->ladder->m_top; + // Sometimes we accidentally run into a ladder without expecting to + if ( me->IsOnLadder() ) + { + 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" diff --git a/src/game/server/neo/bot/neo_bot.cpp b/src/game/server/neo/bot/neo_bot.cpp index 791f62c2ba..dc7291d668 100644 --- a/src/game/server/neo/bot/neo_bot.cpp +++ b/src/game/server/neo/bot/neo_bot.cpp @@ -2587,6 +2587,21 @@ bool CNEOBot::IsEnemy(const CBaseEntity* them) const } } + +bool CNEOBot::IsOnLadder() const +{ + ILocomotion* mover = GetLocomotionInterface(); + if ( !mover ) + { + return false; + } + + return ( GetMoveType() == MOVETYPE_LADDER ) || + mover->IsUsingLadder() || + mover->IsAscendingOrDescendingLadder(); +} + + CNEOBaseCombatWeapon* CNEOBot::GetBludgeonWeapon(void) { return static_cast(Weapon_GetSlot(2)); diff --git a/src/game/server/neo/bot/neo_bot.h b/src/game/server/neo/bot/neo_bot.h index 01f83b0e31..d5a489be9b 100644 --- a/src/game/server/neo/bot/neo_bot.h +++ b/src/game/server/neo/bot/neo_bot.h @@ -172,6 +172,8 @@ class CNEOBot : public NextBotPlayer< CNEO_Player >, public CGameEventListener bool IsEnemy(const CBaseEntity* them) const OVERRIDE; + bool IsOnLadder( ) const; + CNEOBaseCombatWeapon* GetBludgeonWeapon(void); static bool IsBludgeon(CNEOBaseCombatWeapon* pWeapon); diff --git a/src/game/server/neo/bot/neo_bot_locomotion.cpp b/src/game/server/neo/bot/neo_bot_locomotion.cpp index 8ef6c85108..c31685ca7d 100644 --- a/src/game/server/neo/bot/neo_bot_locomotion.cpp +++ b/src/game/server/neo/bot/neo_bot_locomotion.cpp @@ -37,7 +37,15 @@ void CNEOBotLocomotion::Update( void ) } else { +#ifdef NEO + // Don't try to crouch jump if we are climbing a ladder + if (me->GetMoveType() != MOVETYPE_LADDER) + { + me->PressCrouchButton( 0.3f ); + } +#else me->PressCrouchButton( 0.3f ); +#endif } } From 87288cbe39efa97c80f8f1ab006a79e95596af23 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 15 Mar 2026 11:35:44 -0600 Subject: [PATCH 2/3] Code review feedback pass --- .../neo/bot/behavior/neo_bot_ladder_climb.cpp | 124 ++++++------------ .../neo/bot/behavior/neo_bot_ladder_climb.h | 1 + src/game/server/neo/bot/neo_bot.cpp | 2 +- src/game/server/neo/bot/neo_bot.h | 2 +- .../server/neo/bot/neo_bot_locomotion.cpp | 2 +- 5 files changed, 45 insertions(+), 86 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 b1a4714e9c..3be8023c77 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 @@ -188,7 +188,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* } // Check if we're on the ladder (MOVETYPE_LADDER or locomotion says so) - bool onLadder = me->IsOnLadder(); + bool onLadder = me->IsBotOnLadder(); if ( !onLadder ) { @@ -201,7 +201,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* { Vector ladderClosestPoint; CalcClosestPointOnLineSegment( myPos, m_ladder->m_bottom, m_ladder->m_top, ladderClosestPoint ); - if ( myPos.DistToSqr( ladderClosestPoint ) > Square( 100.0f ) ) + if ( myPos.DistToSqr( ladderClosestPoint ) > Square( MAX_DEVIATION_DIST ) ) { return Done( "Fallen too far from ladder, resetting" ); } @@ -227,6 +227,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* if ( distToTarget < mover->GetStepHeight() * 2.0f ) { EnterDismountPhase( me ); + return Continue(); } else { @@ -260,96 +261,60 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* if ( m_bGoingUp ) { body->SetDesiredPosture( IBody::STAND ); + } - float dismountZ = targetZ; - - if ( m_pExitArea && bExitIsBehind ) - { - dismountZ = MIN( m_exitAreaCenter.z, targetZ ); - } - - if ( currentZ >= dismountZ ) - { - EnterDismountPhase( me ); - return Continue(); - } - - bool bIsClimbingDown = false; - if ( m_pExitArea ) + float dismountZ = targetZ; + if ( m_pExitArea ) + { + if ( m_bGoingUp ) { - if ( currentZ < m_exitAreaCenter.z ) - { - me->PressMoveUpButton(); - } - else + if ( bExitIsBehind ) { - me->PressMoveDownButton(); - bIsClimbingDown = true; + dismountZ = Min( m_exitAreaCenter.z, targetZ ); } } else { - me->PressMoveUpButton(); - } - - if ( !bIsClimbingDown ) - { - me->PressForwardButton(); + // Allow early drop-off at intermediate floors + dismountZ = Max( m_exitAreaCenter.z, targetZ ); } - - // Look at the dismount height, slightly behind the ladder - Vector lookTarget = m_ladder->GetPosAtHeight( dismountZ ); - lookTarget -= m_ladder->GetNormal() * 50.0f; - body->AimHeadTowards( lookTarget, IBody::MANDATORY, 0.1f, nullptr, "Climbing up (looking at dismount position)" ); } - else + + // Check if we've reached the target exit height + if ( m_bGoingUp ? ( currentZ >= dismountZ ) : ( currentZ <= dismountZ ) ) { - float dismountZ = targetZ; + EnterDismountPhase( me ); + return Continue(); + } + // Adjust vertical height based on target exit area + bool bIsClimbingDown = false; + if ( onLadder ) + { + bool bShouldGoUp = m_bGoingUp; if ( m_pExitArea ) { - // Allow early drop-off at intermediate floors - dismountZ = MAX( m_exitAreaCenter.z, targetZ ); - } - - // Check if we've reached the bottom or intermediate exit height - if ( currentZ <= dismountZ ) - { - EnterDismountPhase( me ); - return Continue(); + bShouldGoUp = ( currentZ < m_exitAreaCenter.z ); } - // Adjust vertical height based on target exit area - bool bIsClimbingDown = false; - if ( m_pExitArea && onLadder ) + if ( bShouldGoUp ) { - if ( currentZ < m_exitAreaCenter.z ) - { - me->PressMoveUpButton(); - } - else - { - me->PressMoveDownButton(); - bIsClimbingDown = true; - } + me->PressMoveUpButton(); } - else if ( onLadder ) + else { me->PressMoveDownButton(); bIsClimbingDown = true; } + } - // Sanity check: don't press forward if we are actively climbing down - if ( !bIsClimbingDown ) - { - me->PressForwardButton(); - } - // Look at the dismount height, slightly behind the ladder - Vector lookTarget = m_ladder->GetPosAtHeight( dismountZ ); - lookTarget -= m_ladder->GetNormal() * 50.0f; - body->AimHeadTowards( lookTarget, IBody::MANDATORY, 0.1f, nullptr, "Climbing down (looking at dismount position)" ); - } + // Look at and move to the dismount height, slightly behind the ladder + Vector lookTarget = m_ladder->GetPosAtHeight( dismountZ ); + lookTarget -= m_ladder->GetNormal() * 50.0f; + body->AimHeadTowards( lookTarget, IBody::MANDATORY, 0.1f, nullptr, + m_bGoingUp ? "Climbing up (looking at dismount position)" : "Climbing down (looking at dismount position)" ); + me->PressForwardButton(0.1f); } //------------------------------------------------------------ @@ -372,9 +337,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* // Build look target toward exit area center with vertical bias preserved if ( m_pExitArea ) { - Vector dir = m_exitAreaCenter - myPos; - Vector lookTarget = myPos + dir; - + bool bDroppingEarly = ( myPos.z >= m_exitAreaCenter.z ); // Maintain Z-height while on the ladder in the dismount phase if ( onLadder ) { @@ -387,12 +350,15 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* me->PressMoveDownButton(); } } + else + { + bDroppingEarly = true; + } - body->AimHeadTowards( lookTarget, IBody::MANDATORY, 0.1f, nullptr, "Walking to exit area" ); + body->AimHeadTowards( m_exitAreaCenter, IBody::MANDATORY, 0.1f, nullptr, "Walking to exit area" ); // Jump to detach from ladder if exit is not straight ahead, or if we have reached the exit height float dot = DotProduct( toExit, m_ladderForward ); - bool bDroppingEarly = ( myPos.z >= m_exitAreaCenter.z ); if ( dot < 0.5f || bDroppingEarly ) { @@ -401,14 +367,10 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* { me->PressJumpButton(); // mostly to trigger animation if possible - // Zero Z velocity to avoid additive momentum from climbing, reducing overshoots - Vector currentVel = me->GetAbsVelocity(); - currentVel.z = 0.0f; mover->Reset(); // clear velocity cache in locomotion interface - me->SetAbsVelocity( currentVel ); Vector jumpVelocity = toExit * 150.0f; - jumpVelocity.z = 150.0f; + jumpVelocity.z = 150.0f - me->GetAbsVelocity().z; me->ApplyAbsVelocityImpulse( jumpVelocity ); m_bJumpedOffLadder = true; @@ -450,10 +412,6 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* //--------------------------------------------------------------------------------------------- void CNEOBotLadderClimb::EnterDismountPhase( CNEOBot *me ) { - me->ReleaseMoveUpButton(); - me->ReleaseMoveDownButton(); - me->ReleaseForwardButton(); - me->GetLocomotionInterface()->Reset(); // clear velocity cache in locomotion interface me->SetAbsVelocity( vec3_origin ); // stop momentum m_bDismountPhase = true; m_dismountTimer.Start( DISMOUNT_TIMEOUT ); 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 7c14e4e65b..2041154779 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 @@ -49,4 +49,5 @@ class CNEOBotLadderClimb : public Action static constexpr float STUCK_Z_TOLERANCE = 2.0f; static constexpr float DISMOUNT_TIMEOUT = 3.0f; // Max time to walk toward exit area after leaving ladder static constexpr float SAFE_FALL_DIST = 200.0f; // Max height to safely drop off a ladder + static constexpr float MAX_DEVIATION_DIST = 100.0f; // Distance from ladder before canceling }; diff --git a/src/game/server/neo/bot/neo_bot.cpp b/src/game/server/neo/bot/neo_bot.cpp index dc7291d668..0ba42a6f05 100644 --- a/src/game/server/neo/bot/neo_bot.cpp +++ b/src/game/server/neo/bot/neo_bot.cpp @@ -2588,7 +2588,7 @@ bool CNEOBot::IsEnemy(const CBaseEntity* them) const } -bool CNEOBot::IsOnLadder() const +bool CNEOBot::IsBotOnLadder() const { ILocomotion* mover = GetLocomotionInterface(); if ( !mover ) diff --git a/src/game/server/neo/bot/neo_bot.h b/src/game/server/neo/bot/neo_bot.h index d5a489be9b..cc392021a2 100644 --- a/src/game/server/neo/bot/neo_bot.h +++ b/src/game/server/neo/bot/neo_bot.h @@ -172,7 +172,7 @@ class CNEOBot : public NextBotPlayer< CNEO_Player >, public CGameEventListener bool IsEnemy(const CBaseEntity* them) const OVERRIDE; - bool IsOnLadder( ) const; + bool IsBotOnLadder( ) const; CNEOBaseCombatWeapon* GetBludgeonWeapon(void); diff --git a/src/game/server/neo/bot/neo_bot_locomotion.cpp b/src/game/server/neo/bot/neo_bot_locomotion.cpp index c31685ca7d..4ea7047579 100644 --- a/src/game/server/neo/bot/neo_bot_locomotion.cpp +++ b/src/game/server/neo/bot/neo_bot_locomotion.cpp @@ -39,7 +39,7 @@ void CNEOBotLocomotion::Update( void ) { #ifdef NEO // Don't try to crouch jump if we are climbing a ladder - if (me->GetMoveType() != MOVETYPE_LADDER) + if (!me->IsBotOnLadder()) { me->PressCrouchButton( 0.3f ); } From 032cb2a444e5f4201ecbb74d10efe90ee61d8344 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 15 Mar 2026 17:46:56 -0600 Subject: [PATCH 3/3] ResolveExitArea safety checks --- .../server/neo/bot/behavior/neo_bot_ladder_climb.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 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 3be8023c77..db1029ba8d 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 @@ -109,6 +109,9 @@ ActionResult CNEOBotLadderClimb::OnStart( CNEOBot *me, Action //--------------------------------------------------------------------------------------------- void CNEOBotLadderClimb::ResolveExitArea( CNEOBot *me ) { + // Clear potentially stale exit area + m_pExitArea = nullptr; + const PathFollower *path = me->GetCurrentPath(); if ( path && path->IsValid() ) { @@ -118,8 +121,9 @@ void CNEOBotLadderClimb::ResolveExitArea( CNEOBot *me ) return; } + constexpr int MAX_PATH_SEARCH_STEPS = 100; int safetyCounter = 0; - while ( seg && safetyCounter < 100 ) + while ( seg && safetyCounter < MAX_PATH_SEARCH_STEPS ) { if ( !seg->ladder ) { @@ -129,8 +133,9 @@ void CNEOBotLadderClimb::ResolveExitArea( CNEOBot *me ) seg = path->NextSegment( seg ); safetyCounter++; } + Assert( safetyCounter < MAX_PATH_SEARCH_STEPS ); - if ( seg && seg->area ) + if ( seg && !seg->ladder && seg->area ) { m_pExitArea = seg->area; m_exitAreaCenter = m_pExitArea->GetCenter();