From a16ba3e0c4a1f1626f53578516cb31562121b959 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 19:05:29 -0600 Subject: [PATCH] Step off a ladder crouched through a crawl gap A ladder whose top exit is a gap only a crouched body fits through, such as a vent or a low window, was never left: the dismount check sweeps the standing body, finds the gap's top in the way, keeps the bot climbing past the ladder's top, and the stall check then jumps it off. When the standing sweep is blocked but a crouched body at the ladder's top clears, duck, hold the feet just above the top and move out toward the landing (along the face for an exit beside the ladder, where a press into the face would climb). Seen on ntre_rise_ctg ladder 3, whose exit is a 52 u window. --- .../neo/bot/behavior/neo_bot_ladder_climb.cpp | 119 +++++++++++++++++- .../neo/bot/behavior/neo_bot_ladder_climb.h | 12 ++ 2 files changed, 129 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 09a5b22501..c0f0d9b995 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 @@ -17,7 +17,7 @@ static constexpr float LADDER_PLACE_SEEK = 16.0f; //--------------------------------------------------------------------------------------------- CNEOBotLadderClimb::CNEOBotLadderClimb( const CNavLadder *ladder, bool goingUp ) : m_ladder( ladder ), m_bGoingUp( goingUp ), m_flLastZ( 0.0f ), - m_bDismountPhase( false ), m_bJumpedOffLadder( false ), m_pExitArea( nullptr ), m_nNudges( 0 ) + m_bDismountPhase( false ), m_bJumpedOffLadder( false ), m_bCrawlDismount( false ), m_bCrawlPushed( false ), m_pExitArea( nullptr ), m_nNudges( 0 ) { m_dismountPos = vec3_origin; m_ladderForward = ladder ? -ladder->GetNormal() : vec3_origin; @@ -355,7 +355,8 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* if ( !onLadder ) { - if ( mover->IsOnGround() ) + // crawling off, the gap's sill is ground too, short of the landing + if ( mover->IsOnGround() && !m_bCrawlDismount ) { return Done( "Reached the ground, ending climb" ); } @@ -472,6 +473,20 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* bWantsDismount = true; } + // A gap at the top only a crouched body fits through, such as a vent or a low window: climbing on never clears it, + // so duck on the way up, and step off crouched once the gap is in reach + if ( m_bGoingUp && onLadder && m_pExitArea && currentZ > GetCrawlFeetZ() - CRAWL_DUCK_RANGE && IsCrawlOnlyExit( me, toExit ) ) + { + me->PressCrouchButton( CRAWL_CROUCH_HOLD ); + if ( ( me->GetFlags() & FL_DUCKING ) && fabsf( GetCrawlFeetZ() - currentZ ) <= CRAWL_SNAP_DIST ) + { + m_bCrawlDismount = true; + EnterDismountPhase( me ); + UpdateCrawlDismount( me, toExit, onLadder ); + return Continue(); + } + } + // A parapet between the ladder and a roof exit blocks the step off at floor height: // keep climbing until the hull clears it (stuck detection ends it if nothing does) const bool bLipInTheWay = bWantsDismount && m_bGoingUp && onLadder @@ -562,6 +577,12 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* return Done( "Dismount walk timed out" ); } + if ( m_bCrawlDismount && m_pExitArea ) + { + UpdateCrawlDismount( me, toExit, onLadder ); + return Continue(); + } + // Build look target toward exit area center with vertical bias preserved if ( m_pExitArea ) { @@ -649,6 +670,100 @@ bool CNEOBotLadderClimb::IsDismountBlocked( CNEOBot *me, const Vector &toExit ) return tr.DidHit(); } +//--------------------------------------------------------------------------------------------- +// Which way to crawl off toward the exit: along the face for an exit beside the ladder, +// since on the ladder a press with any part into the face climbs +Vector CNEOBotLadderClimb::GetCrawlDirection( const Vector &toExit ) const +{ + if ( DotProduct( toExit, m_ladderForward ) >= 0.5f ) + { + return toExit; + } + + const Vector &normal = m_ladder->GetNormal(); + Vector along = toExit - DotProduct( toExit, normal ) * normal; + along.z = 0.0f; + return ( along.NormalizeInPlace() > 0.0f ) ? along : toExit; +} + +//--------------------------------------------------------------------------------------------- +// Where the feet step off crawling: just above the ladder's top +float CNEOBotLadderClimb::GetCrawlFeetZ() const +{ + return m_ladder->m_top.z + CRAWL_FEET_ABOVE_TOP; +} + +//--------------------------------------------------------------------------------------------- +// From the ladder's top toward the exit, the standing body is blocked but a crouched one is clear +bool CNEOBotLadderClimb::IsCrawlOnlyExit( CNEOBot *me, const Vector &toExit ) const +{ + const IBody *body = me->GetBodyInterface(); + const float halfWidth = body->GetHullWidth() * 0.5f; + const Vector mins( -halfWidth, -halfWidth, 0.0f ); + const Vector standMaxs( halfWidth, halfWidth, body->GetStandHullHeight() ); + const Vector crouchMaxs( halfWidth, halfWidth, body->GetCrouchHullHeight() ); + + Vector start = me->GetLocomotionInterface()->GetFeet(); + start.z = GetCrawlFeetZ(); + const Vector end = start + GetCrawlDirection( toExit ) * ( DISMOUNT_CLEARANCE_HULLS * body->GetHullWidth() ); + + trace_t tr; + UTIL_TraceHull( start, end, mins, standMaxs, MASK_NPCSOLID, me, COLLISION_GROUP_NONE, &tr ); + if ( !tr.DidHit() ) + { + return false; + } + + UTIL_TraceHull( start, end, mins, crouchMaxs, MASK_NPCSOLID, me, COLLISION_GROUP_NONE, &tr ); + return !tr.DidHit(); +} + +//--------------------------------------------------------------------------------------------- +// Bot updates come a tenth of a second apart, too coarse to stop a climb inside the gap's few units, +// so the crouched bot is set just above the ladder's top, let go of the ladder and pushed into the gap: +// +// |#####| <- the gap's top +// ladder | o-> | landing +// || |_____|________ <- the gap's sill, the ladder's top +// || |#####| +void CNEOBotLadderClimb::UpdateCrawlDismount( CNEOBot *me, const Vector &toExit, bool bOnLadder ) +{ + me->PressCrouchButton( CRAWL_CROUCH_HOLD ); + if ( bOnLadder && !m_bCrawlPushed ) + { + m_bCrawlPushed = true; + + Vector crawlPos = me->GetLocomotionInterface()->GetFeet(); + crawlPos.z = GetCrawlFeetZ(); + trace_t tr; + UTIL_TraceHull( crawlPos, crawlPos, me->WorldAlignMins(), me->WorldAlignMaxs(), MASK_PLAYERSOLID, me, COLLISION_GROUP_PLAYER_MOVEMENT, &tr ); + if ( tr.startsolid ) + { + return; + } + + me->ReleaseMoveUpButton(); + me->ReleaseForwardButton(); + me->SetAbsOrigin( crawlPos ); + me->SetMoveType( MOVETYPE_WALK ); + + // with a little rise, so the bot reaches the sill before it falls below it + Vector push = GetCrawlDirection( toExit ) * CRAWL_PUSH_SPEED; + push.z = CRAWL_PUSH_RISE_SPEED; + me->SetAbsVelocity( push ); + return; + } + + // Level, so the bot looks where it walks. In the air beside the ladder a press toward its face grabs it again, + // so the push alone carries the bot until it lands + me->GetBodyInterface()->AimHeadTowards( me->EyePosition() + toExit * 100.0f, IBody::MANDATORY, 0.1f, nullptr, "Crawling off the ladder" ); + ILocomotion *mover = me->GetLocomotionInterface(); + if ( mover->IsOnGround() ) + { + mover->Approach( mover->GetFeet() + toExit * 100.0f ); + } +} + //--------------------------------------------------------------------------------------------- void CNEOBotLadderClimb::EnterDismountPhase( CNEOBot *me ) { 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 f95510f443..702db24dda 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 @@ -35,6 +35,10 @@ class CNEOBotLadderClimb : public Action bool IsNarrowLanding( CNEOBot *me ) const; Vector GetDismountPos( CNEOBot *me ) const; bool IsDismountBlocked( CNEOBot *me, const Vector &toExit ) const; + Vector GetCrawlDirection( const Vector &toExit ) const; + float GetCrawlFeetZ() const; + bool IsCrawlOnlyExit( CNEOBot *me, const Vector &toExit ) const; + void UpdateCrawlDismount( CNEOBot *me, const Vector &toExit, bool bOnLadder ); void ClaimLadder( CNEOBot *me ) const; const CNavLadder *m_ladder; @@ -43,6 +47,8 @@ class CNEOBotLadderClimb : public Action bool m_bGoingUp; bool m_bDismountPhase; bool m_bJumpedOffLadder; + bool m_bCrawlDismount; // stepping off crouched, through a gap only a crouched body fits + bool m_bCrawlPushed; // set in the gap and pushed off the ladder float m_flLastZ; Vector m_dismountPos; @@ -62,5 +68,11 @@ class CNEOBotLadderClimb : public Action static constexpr float DISMOUNT_CLEARANCE_HULLS = 1.5f; // Hull widths toward the exit that must be clear to step off static constexpr float NARROW_LANDING_HULLS = 0.5f; // Hull widths deep a landing must be to stand on (measured on dawn and dusk L1's 7 u wall cap) static constexpr float LANDING_INSET = 16.0f; // How far inside a landing's near edge the dismount aims: half a hull, so the whole hull lands + static constexpr float CRAWL_FEET_ABOVE_TOP = 4.0f; // Crouched, the feet step off this far above the ladder's top + static constexpr float CRAWL_DUCK_RANGE = 64.0f; // How far below that height the climb ducks for a crawl exit: a few bot updates' climb + static constexpr float CRAWL_SNAP_DIST = 24.0f; // How far from that height the bot may be set there: a little over a bot update's climb + static constexpr float CRAWL_PUSH_SPEED = 150.0f; // How fast the bot is pushed off the ladder into the gap, + static constexpr float CRAWL_PUSH_RISE_SPEED = 40.0f; // and up, a unit's rise + static constexpr float CRAWL_CROUCH_HOLD = 0.3f; // Seconds each crouch press lasts, outliving a bot update static constexpr int STALL_NUDGES = 3; // Sideways shimmies before a stalled climb jumps off (measured on subsurface L1 and saitama L2) };