diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.cpp b/src/game/server/NextBot/Path/NextBotPathFollow.cpp index da57ce9281..a8efd479dd 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.cpp +++ b/src/game/server/NextBot/Path/NextBotPathFollow.cpp @@ -34,6 +34,9 @@ ConVar NextBotDebugClimbing( "nb_debug_climbing", "0", FCVAR_CHEAT ); #ifdef NEO // How far from its feet a bot that landed looks for a nav area, the range UpdateLastKnownArea() searches static const float LANDING_NAV_AREA_RANGE = 50.0f; +// A climb onto a ladder's wall cap takes the mesh's link at most this many times a path segment, +// then is left to the ledge search: a bot that lands back at the launch point would otherwise jump on every landing +static const int LADDER_CAP_LAUNCHES = 3; #endif @@ -57,6 +60,8 @@ PathFollower::PathFollower( void ) #ifdef NEO m_wasOnGround = true; + m_capClimbGoal = NULL; + m_capLaunches = 0; #endif } @@ -121,6 +126,10 @@ void PathFollower::OnPathChanged( INextBot *bot, Path::ResultType result ) #ifdef NEO m_pathObstacles.Reset(); + + // a new path reuses the segments' storage, so a launch count kept by segment starts over + m_capClimbGoal = NULL; + m_capLaunches = 0; #endif } @@ -1284,6 +1293,27 @@ bool PathFollower::FindClimbLedge( INextBot *bot, Vector startTracePos, Vector l #endif // _DEBUG +#ifdef NEO +//-------------------------------------------------------------------------------------------------------------- +// The area after the goal when it is a ladder top less than a hull deep along the approach, else NULL: +// the ledge search cannot find such an area (not the dismount's IsNarrowLanding(), which asks whether a bot can stand on it) +const CNavArea *PathFollower::LadderCapAhead( INextBot *bot ) const +{ + // an area a ladder goes down from is that ladder's top + const Segment *afterClimb = NextSegment( m_goal ); + if ( !afterClimb || !afterClimb->area || afterClimb->area->GetLadders( CNavLadder::LADDER_DOWN )->Count() == 0 ) + { + return NULL; + } + + // the depth is the area's axis-aligned size along the approach's dominant axis + const CNavArea *cap = afterClimb->area; + const Vector toLedge = cap->GetCenter() - bot->GetLocomotionInterface()->GetFeet(); + const float flDepth = ( fabsf( toLedge.x ) > fabsf( toLedge.y ) ) ? cap->GetSizeX() : cap->GetSizeY(); + return ( flDepth < bot->GetBodyInterface()->GetHullWidth() ) ? cap : NULL; +} +#endif + //-------------------------------------------------------------------------------------------------------------- /** * Climb up ledges @@ -1349,6 +1379,34 @@ bool PathFollower::Climbing( INextBot *bot, const Path::Segment *goal, const Vec return false; } +#ifdef NEO + // The ledge search below looks for a ledge's floor a full look-ahead past its wall and misses a wall cap, + // so at the launch point of a climb onto one, take the mesh's climb link as the authoritative branch does + const CNavArea *cap = ( m_goal->type == CLIMB_UP ) ? LadderCapAhead( bot ) : NULL; + if ( cap && m_goal != m_capClimbGoal ) + { + m_capClimbGoal = m_goal; + m_capLaunches = 0; + } + + if ( cap && m_capLaunches < LADDER_CAP_LAUNCHES + && ( m_goal->pos - mover->GetFeet() ).AsVector2D().IsLengthLessThan( body->GetHullWidth() ) ) + { + // the authoritative branch's lines, repeated to leave that branch untouched + Vector nearClimbGoal; + cap->GetClosestPointOnArea( mover->GetFeet(), &nearClimbGoal ); + + climbDirection = nearClimbGoal - mover->GetFeet(); + climbDirection.z = 0.0f; + climbDirection.NormalizeInPlace(); + + if ( mover->ClimbUpToLedge( nearClimbGoal, climbDirection, NULL ) ) + { + ++m_capLaunches; + return true; + } + } +#endif // If we're approaching a CLIMB_UP link, save off the height delta for it, and trust the nav *just* enough // to climb up to that ledge and only that ledge. We keep as large a tolerance as possible, to trust diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.h b/src/game/server/NextBot/Path/NextBotPathFollow.h index c421f82852..9ea62157d0 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.h +++ b/src/game/server/NextBot/Path/NextBotPathFollow.h @@ -89,8 +89,11 @@ class PathFollower : public Path #ifdef NEO bool HasFallenBelowPath( INextBot *bot ) const; // return true if we stand off the path, more than a step below it + const CNavArea *LadderCapAhead( INextBot *bot ) const; // return the ladder top shallower than a hull the climb ahead is onto, or NULL bool m_wasOnGround; CNEOBotPathObstacles m_pathObstacles; // looks ahead for props to steer around and breakables in the way + const Path::Segment *m_capClimbGoal; // the climb onto a ladder's wall cap m_capLaunches counts + int m_capLaunches; #endif float m_goalTolerance; diff --git a/src/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp b/src/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp index ed83abb3af..851ec08576 100644 --- a/src/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp +++ b/src/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp @@ -17,6 +17,29 @@ ConVar NextBotPlayerMoveDirect( "nb_player_move_direct", "0" ); +#ifdef NEO +// How long a bot must be held against a ladder it did not ask for before HandleUnwantedLadder() takes it over: +// a real snag is caught within half a second, and contacts that clear themselves are left to the ordinary release +static constexpr float LADDER_ADOPT_TIME = 0.5f; +// A ladder further than this from the bot is not the one it is stuck on. +static constexpr float LADDER_TOUCH_RANGE = 64.0f; +// A gap longer than this means the bot walked away and came back, so the bout starts over. +static constexpr float LADDER_CONTACT_RESET = 1.0f; + +// Going down a ladder, forward is pressed only once it moves the bot down at least this fraction of the climb speed +// (facing the face square on, a view more than 55 degrees below level): slower than that, the view is still coming round. +static constexpr float LADDER_MIN_DESCENT_RATE = 0.25f; + +// A catch over a ladder's top edge lapses this long after the descent last asked for it. +static constexpr float LADDER_CATCH_TIME = 0.5f; +// How long the move down is held from a catch: past the grab, until the climb takes the ladder over. +static constexpr float LADDER_CATCH_DESCENT_TIME = 0.2f; +// How far the hull may still reach back over the ladder's top, twice LadderMove()'s 2 u reach: closer in, it could be standing on it. +static constexpr float LADDER_CATCH_OVERLAP = 4.0f; +// The view must point at least this much into the face (cos 45 degrees) for forward to reach it. +static constexpr float LADDER_CATCH_FACING = 0.707f; +#endif // NEO + //----------------------------------------------------------------------------------------------------- PlayerLocomotion::PlayerLocomotion( INextBot *bot ) : ILocomotion( bot ) { @@ -45,6 +68,13 @@ void PlayerLocomotion::Reset( void ) m_ladderDismountGoal = NULL; m_ladderTimer.Invalidate(); +#ifdef NEO + m_unwantedLadderSince = 0.0f; + m_unwantedLadderLastTouch = 0.0f; + m_ladderToCatch = NULL; + m_ladderCatchTimer.Invalidate(); +#endif + m_minSpeedLimit = 0.0f; m_maxSpeedLimit = 9999999.9f; @@ -52,6 +82,209 @@ void PlayerLocomotion::Reset( void ) } +#ifdef NEO +//----------------------------------------------------------------------------------------------------- +/** + * The nav ladder the bot is standing in, or NULL. The engine knows which brush it grabbed but + * keeps that normal private, so match against the nav record instead - close in xy, and inside the + * ladder's own height span. + */ +const CNavLadder *PlayerLocomotion::FindTouchedLadder( void ) const +{ + const Vector &feet = GetFeet(); + const CNavLadder *best = NULL; + float bestRangeSq = LADDER_TOUCH_RANGE * LADDER_TOUCH_RANGE; + + // The grab only needs the hull to touch the brush, so a bot standing well below + // a ladder whose foot hangs over a walkway is a case to catch, not to filter out + const float below = GetBot()->GetBodyInterface()->GetStandHullHeight(); + + for ( int i = 0; i < TheNavMesh->GetLadders().Count(); ++i ) + { + const CNavLadder *ladder = TheNavMesh->GetLadders()[i]; + + if ( feet.z < ladder->m_bottom.z - below || feet.z > ladder->m_top.z + GetStepHeight() ) + { + continue; + } + + const float rangeSq = ( ladder->GetPosAtHeight( feet.z ) - feet ).AsVector2D().LengthSqr(); + if ( rangeSq < bestRangeSq ) + { + bestRangeSq = rangeSq; + best = ladder; + } + } + + return best; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Called when the engine has us on a ladder our own path never asked for. Returns true if the + * ladder was taken over, in which case the caller leaves the move type alone and the normal ladder + * state machine drives from here. + */ +bool PlayerLocomotion::HandleUnwantedLadder( void ) +{ + // Most grabs clear themselves within a second, and adopting them buys climbing nobody asked for, + // so only take the ladder over once the bot has been held against it + const float now = gpGlobals->curtime; + + if ( now - m_unwantedLadderLastTouch > LADDER_CONTACT_RESET ) + { + m_unwantedLadderSince = now; + } + + m_unwantedLadderLastTouch = now; + + if ( now - m_unwantedLadderSince < LADDER_ADOPT_TIME ) + { + return false; + } + + // The scan over every nav ladder is needed only to take one over, so it waits for the bout + const CNavLadder *ladder = FindTouchedLadder(); + if ( ladder == NULL ) + { + return false; + } + + // Leave by the nearer end, the one the bot reaches soonest + // (always climbing out upward was measured: it sends bots where they must come back from, and costs captures) + const bool bGoUp = ( GetFeet().z - ladder->m_bottom.z ) > ( ladder->m_top.z - GetFeet().z ); + + const CNavArea *dismount = bGoUp ? ladder->GetTopArea() : ladder->m_bottomArea; + if ( dismount == NULL ) + { + return false; + } + + m_ladderInfo = ladder; + m_ladderDismountGoal = dismount; + m_ladderState = bGoUp ? ASCENDING_LADDER : DESCENDING_LADDER; + + return true; +} + + +//----------------------------------------------------------------------------------------------------- +// The horizontal direction into the face of the ladder brush we hold. LadderMove() takes whichever +// face we touched, so a bot that grabbed the ladder from behind holds its back face. +Vector PlayerLocomotion::GetIntoLadderFace( const CNavLadder *ladder ) const +{ + const Vector &normal = ladder->GetNormal(); + const bool bBehind = DotProduct( GetFeet() - ladder->m_top, normal ) < 0.0f; + + return bBehind ? normal : -normal; +} + + +//----------------------------------------------------------------------------------------------------- +// Would pressing forward move us down this ladder? LadderMove() turns a push into the face into +// climbing up, so forward descends only while the view points further down than into the face. +bool PlayerLocomotion::IsForwardDownLadder( const CNavLadder *ladder ) const +{ + Vector view; + m_player->EyeVectors( &view ); + + // Facing away from the face is no good either: standing on a floor, LadderMove() pushes us off for it + const float intoFace = DotProduct( view, GetIntoLadderFace( ladder ) ); + if ( intoFace < 0.0f ) + { + return false; + } + + // As CGameMovement::LadderMove() turns a move on an upright ladder: the part into the face climbs, + // and the view's own vertical part adds to it, so this is a forward press's vertical speed as a fraction of the climb speed + const float climbRate = view.z + intoFace; + + return climbRate < -LADDER_MIN_DESCENT_RATE; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * A descent backing out over the ladder's top edge asks for this every update: Upkeep() presses into + * the face on the tick the bot drops past the top. + */ +void PlayerLocomotion::CatchLadderBelowTop( const CNavLadder *ladder ) +{ + m_ladderToCatch = ladder; + m_ladderCatchTimer.Start( LADDER_CATCH_TIME ); +} + + +//----------------------------------------------------------------------------------------------------- +void PlayerLocomotion::StopCatchingLadder( void ) +{ + m_ladderToCatch = NULL; + m_ladderCatchTimer.Invalidate(); +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Bot updates come ten times a second, and a bot backing off the top is out of LadderMove()'s 2 u reach + * of the face within a tick or two: so the press into the face is made here, every tick, until it grabs. + */ +void PlayerLocomotion::Upkeep( void ) +{ + BaseClass::Upkeep(); + + const CNavLadder *ladder = m_ladderToCatch; + if ( ladder == NULL ) + { + return; + } + + if ( m_ladderCatchTimer.IsElapsed() || m_player->GetMoveType() == MOVETYPE_LADDER ) + { + StopCatchingLadder(); + return; + } + + INextBotPlayerInput *playerButtons = dynamic_cast< INextBotPlayerInput * >( GetBot() ); + if ( playerButtons == NULL ) + { + return; + } + + // Forward has to point into the face, or pressing it reaches nothing + const Vector &normal = ladder->GetNormal(); + Vector view; + m_player->EyeVectors( &view ); + if ( -DotProduct2D( view.AsVector2D(), normal.AsVector2D() ) < LADDER_CATCH_FACING ) + { + return; + } + + // In the air below the top, in front of the face with the hull clear of the ladder's own top, and not + // beside the ladder. Below the top by a step's margin: a ladder brush often stands above its nav ladder's top + const Vector &feet = GetFeet(); + if ( IsOnGround() || feet.z >= ladder->m_top.z + GetStepHeight() ) + { + return; + } + + const Vector toFeet = feet - ladder->GetPosAtHeight( feet.z ); + const float side = toFeet.x * normal.y - toFeet.y * normal.x; + const float halfHull = GetBot()->GetBodyInterface()->GetHullWidth() * 0.5f; + if ( DotProduct( toFeet, normal ) < halfHull - LADDER_CATCH_OVERLAP + || fabsf( side ) > ladder->m_width * 0.5f + halfHull ) + { + return; + } + + // LadderMove() grabs on the tick the hull is within reach, and the move down makes that first + // ladder move a descent whatever the view + playerButtons->PressForwardButton(); + playerButtons->PressMoveDownButton( LADDER_CATCH_DESCENT_TIME ); +} +#endif // NEO + + //----------------------------------------------------------------------------------------------------- bool PlayerLocomotion::TraverseLadder( void ) { @@ -88,6 +321,14 @@ bool PlayerLocomotion::TraverseLadder( void ) if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER ) { // on ladder and don't want to be +#ifdef NEO + // HandleUnwantedLadder() may take the ladder over instead, + // in which case the state machine drives from here and we are done + if ( HandleUnwantedLadder() ) + { + break; + } +#endif GetBot()->GetEntity()->SetMoveType( MOVETYPE_WALK ); } return false; @@ -115,6 +356,15 @@ PlayerLocomotion::LadderState PlayerLocomotion::ApproachAscendingLadder( void ) return DISMOUNTING_LADDER_TOP; } +#ifdef NEO + // A bot already on the ladder is not too far below it, whatever the nav ladder's bottom says - + // on a ladder whose foot hangs above the floor the check below would drop a climb the bot has begun + if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER ) + { + return ASCENDING_LADDER; + } +#endif + // sanity check - are we too far below this ladder to reach it? if ( GetFeet().z <= m_ladderInfo->m_bottom.z - GetMaxJumpHeight() ) { @@ -149,6 +399,15 @@ PlayerLocomotion::LadderState PlayerLocomotion::ApproachDescendingLadder( void ) return NO_LADDER; } +#ifdef NEO + // A bot already on the ladder climbs down it. Steering on towards the mount point presses forward + // with the view still level, which climbs it up and off the top; letting go drops it from there. + if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER ) + { + return DESCENDING_LADDER; + } +#endif + // sanity check - are we already at the end of this ladder? if ( GetFeet().z <= m_ladderInfo->m_bottom.z + GetMaxJumpHeight() ) { @@ -281,7 +540,12 @@ PlayerLocomotion::LadderState PlayerLocomotion::DescendLadder( void ) } // climb down this ladder - look down +#ifdef NEO + // into its face (facing away, a bot still on the top floor is pushed off the ladder the moment it presses forward) + Vector goal = GetFeet() + 100.0f * ( GetIntoLadderFace( m_ladderInfo ) + Vector( 0, 0, -2 ) ); +#else Vector goal = GetFeet() + 100.0f * ( m_ladderInfo->GetNormal() + Vector( 0, 0, -2 ) ); +#endif GetBot()->GetBodyInterface()->AimHeadTowards( goal, IBody::MANDATORY, 0.1f, NULL, "Ladder" ); @@ -489,7 +753,15 @@ void PlayerLocomotion::Approach( const Vector &pos, float goalWeight ) } #endif - if ( m_player->IsOnLadder() && IsUsingLadder() && ( m_ladderState == ASCENDING_LADDER || m_ladderState == DESCENDING_LADDER ) ) +#ifdef NEO + // On the way down, press nothing until forward would move us down: + // with the view still coming round it climbs us up, and from the top of the ladder, off it + const bool bHoldForView = m_player->IsOnLadder() && m_ladderState == DESCENDING_LADDER + && m_ladderInfo && !IsForwardDownLadder( m_ladderInfo ); +#else + const bool bHoldForView = false; +#endif + if ( !bHoldForView && m_player->IsOnLadder() && IsUsingLadder() && ( m_ladderState == ASCENDING_LADDER || m_ladderState == DESCENDING_LADDER ) ) { // we are on a ladder and WANT to be on a ladder. playerButtons->PressForwardButton(); @@ -525,7 +797,7 @@ void PlayerLocomotion::Approach( const Vector &pos, float goalWeight ) } } } - else + else if ( !bHoldForView ) { const float epsilon = 0.25f; if ( NextBotPlayerMoveDirect.GetBool() ) @@ -795,6 +1067,12 @@ void PlayerLocomotion::ClimbLadder( const CNavLadder *ladder, const CNavArea *di // Approach( goal ); // FaceTowards( goal ); +#ifdef NEO + // A climb asked for ends any bout of unwanted contact, so the next one waits LADDER_ADOPT_TIME afresh + m_unwantedLadderSince = 0.0f; + m_unwantedLadderLastTouch = 0.0f; +#endif + m_ladderState = APPROACHING_ASCENDING_LADDER; m_ladderInfo = ladder; m_ladderDismountGoal = dismountGoal; @@ -812,6 +1090,12 @@ void PlayerLocomotion::DescendLadder( const CNavLadder *ladder, const CNavArea * // Approach( goal ); // FaceTowards( goal ); +#ifdef NEO + // A climb asked for ends any bout of unwanted contact, so the next one waits LADDER_ADOPT_TIME afresh + m_unwantedLadderSince = 0.0f; + m_unwantedLadderLastTouch = 0.0f; +#endif + m_ladderState = APPROACHING_DESCENDING_LADDER; m_ladderInfo = ladder; m_ladderDismountGoal = dismountGoal; diff --git a/src/game/server/NextBot/Player/NextBotPlayerLocomotion.h b/src/game/server/NextBot/Player/NextBotPlayerLocomotion.h index 6a947835ca..03e9b3f8de 100644 --- a/src/game/server/NextBot/Player/NextBotPlayerLocomotion.h +++ b/src/game/server/NextBot/Player/NextBotPlayerLocomotion.h @@ -61,6 +61,12 @@ class PlayerLocomotion : public ILocomotion virtual bool IsUsingLadder( void ) const; virtual bool IsAscendingOrDescendingLadder( void ) const; // we are actually on the ladder right now, either climbing up or down virtual bool IsAbleToAutoCenterOnLadder( void ) const; +#ifdef NEO + bool IsForwardDownLadder( const CNavLadder *ladder ) const; // would pressing forward move us down this ladder + void CatchLadderBelowTop( const CNavLadder *ladder ); // press into this ladder's face the tick we drop past its top + void StopCatchingLadder( void ); + virtual void Upkeep( void ) override; +#endif virtual void FaceTowards( const Vector &target ); // rotate body to face towards "target" @@ -127,6 +133,20 @@ class PlayerLocomotion : public ILocomotion const CNavArea *m_ladderDismountGoal; CountdownTimer m_ladderTimer; // a "give up" timer if things go awry +#ifdef NEO + // a ladder the engine put us on that our own path never asked for: releasing it is not enough, + // since CGameMovement::LadderMove() re-grabs from the bot's wish direction (see TraverseLadder()) + bool HandleUnwantedLadder( void ); + const CNavLadder *FindTouchedLadder( void ) const; + Vector GetIntoLadderFace( const CNavLadder *ladder ) const; // into the face of the ladder brush we hold + float m_unwantedLadderSince; // when the current bout of unwanted contact began + float m_unwantedLadderLastTouch; // last tick we were on a ladder we did not ask for + + // a descent backing out over the ladder's top edge: Upkeep() presses into the face the tick we drop past the top + const CNavLadder *m_ladderToCatch; + CountdownTimer m_ladderCatchTimer; +#endif + bool IsClimbPossible( INextBot *me, const CBaseEntity *obstacle ) const; }; diff --git a/src/game/server/nav_ladder.h b/src/game/server/nav_ladder.h index 91c456f94c..295dc90b9d 100644 --- a/src/game/server/nav_ladder.h +++ b/src/game/server/nav_ladder.h @@ -65,6 +65,9 @@ class CNavLadder CNavArea *m_topRightArea; CNavArea *m_topBehindArea; ///< area at top of ladder "behind" it - only useful for descending CNavArea *m_bottomArea; ///< the area at the bottom of the ladder +#ifdef NEO + const CNavArea *GetTopArea( void ) const; ///< the first top area set, trying the one behind last +#endif bool IsConnected( const CNavArea *area, LadderDirectionType dir ) const; ///< returns true if given area is connected in given direction @@ -150,6 +153,18 @@ inline const Vector &CNavLadder::GetNormal( void ) const return m_normal; } #ifdef NEO + +//-------------------------------------------------------------------------------------------------------------- +// nav_generate puts a ladder's top area in whichever slot its landing is in, often not the forward one, +// and some ladders only have the area behind their plane (such as one up a shaft) +inline const CNavArea *CNavLadder::GetTopArea( void ) const +{ + return m_topForwardArea ? m_topForwardArea + : m_topLeftArea ? m_topLeftArea + : m_topRightArea ? m_topRightArea + : m_topBehindArea; +} + class CInfoLadder : public CBaseEntity { public: 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 27d9c05ee2..347fb74cb4 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 @@ -9,7 +9,7 @@ //--------------------------------------------------------------------------------------------- CNEOBotLadderApproach::CNEOBotLadderApproach( const CNavLadder *ladder, bool goingUp ) - : m_ladder( ladder ), m_bGoingUp( goingUp ) + : m_ladder( ladder ), m_bGoingUp( goingUp ), m_bOverTop( false ), m_bFlushTop( false ), m_vecLastProgressPos( vec3_origin ) { m_ladderCenter = ladder ? ( ladder->m_top + ladder->m_bottom ) * 0.5f : vec3_origin; } @@ -25,6 +25,29 @@ ActionResult CNEOBotLadderApproach::OnStart( CNEOBot *me, ActionGetLocomotionInterface(); + const Vector &feet = mover->GetFeet(); + const CNavArea *area = me->GetLastKnownArea(); + const float floorZ = area ? MIN( feet.z, area->GetZ( feet.x, feet.y ) ) : feet.z; + const Vector2D toFeet = ( feet - m_ladder->m_top ).AsVector2D(); + const bool bBehind = DotProduct2D( toFeet, m_ladder->GetNormal().AsVector2D() ) < 0.0f; + + m_bOverTop = bBehind && floorZ < m_ladder->m_top.z + mover->GetStepHeight(); + m_bFlushTop = m_bOverTop && floorZ >= m_ladder->m_top.z - mover->GetStepHeight(); + if ( m_bOverTop ) + { + m_timeoutTimer.Start( OVER_TOP_TIMEOUT ); + } + } + + m_vecLastProgressPos = me->GetLocomotionInterface()->GetFeet(); + m_progressTimer.Start(); + if ( me->IsDebugging( NEXTBOT_PATH ) ) { DevMsg( "%s: Starting ladder approach (%s), length %.1f\n", @@ -50,11 +73,27 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float ) return Done( "Ladder is invalid" ); } + // The pre-round freeze holds the bot in place, so wait it out: counted as a stall, + // it drops a descent over a level top back to the ordinary approach, which walks off the top + if ( me->GetNeoFlags() & NEO_FL_FREEZETIME ) + { + m_timeoutTimer.Reset(); + m_progressTimer.Start(); + return Continue(); + } + if ( m_timeoutTimer.IsElapsed() ) { return Done( "Ladder approach timeout" ); } + const Vector &feetNow = me->GetLocomotionInterface()->GetFeet(); + if ( ( feetNow - m_vecLastProgressPos ).AsVector2D().IsLengthGreaterThan( PROGRESS_STEP ) ) + { + m_vecLastProgressPos = feetNow; + m_progressTimer.Start(); + } + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true); if ( threat ) { @@ -86,6 +125,12 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float ) // Are we climbing up or down the ladder? Vector targetPos = m_bGoingUp ? m_ladder->m_bottom : m_ladder->m_top; + // Going down, head for where the bot will hang on the ladder: in front of its face, over the drop + if ( !m_bGoingUp ) + { + targetPos += m_ladder->GetNormal() * ( body->GetHullWidth() * 0.5f + HANG_CLEARANCE ); + } + // Calculate 2D vector from bot to ladder mount point Vector2D to = ( targetPos - myPos ).AsVector2D(); float range = to.NormalizeInPlace(); @@ -94,11 +139,24 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float ) Vector2D ladderNormal2D = m_ladder->GetNormal().AsVector2D(); float dot = DotProduct2D( ladderNormal2D, to ); + if ( m_bOverTop ) + { + return UpdateOverTop( me, targetPos, range ); + } + // 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; + + // Going down, look down the ladder instead: the forward press that grabs it stays held for a few + // ticks, and with a level view it climbs the bot up and off the top + if ( !m_bGoingUp ) + { + lookTarget = m_ladder->m_bottom; + } + body->AimHeadTowards( lookTarget, IBody::MANDATORY, 0.1f, nullptr, "Stare at ladder center" ); if ( me->IsDebugging( NEXTBOT_PATH ) ) @@ -107,8 +165,47 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float ) NDebugOverlay::Line( myPos, targetPos, 255, 255, 0, true, 0.1f ); } - // Are we aligned and close enough to mount the ladder? - if ( range >= MOUNT_RANGE ) + // Going up a ladder whose foot hangs above the floor, a crouched bot fits under the ladder and never touches it, + // so stand up, and step out from under it first (a bot drops onto the floor there crouched, from a climb down) + if ( m_bGoingUp && m_ladder->m_bottom.z > myPos.z + mover->GetStepHeight() ) + { + me->ReleaseCrouchButton(); + if ( me->GetFlags() & FL_DUCKING ) + { + Vector clear = m_ladder->m_bottom + m_ladder->GetNormal() * ( body->GetHullWidth() * 0.5f + HANG_CLEARANCE ); + clear.z = myPos.z; + mover->Approach( clear ); + return Continue(); + } + } + + // Going down, mount only once off the top: grabbed while still up there, on the landing or on the + // ladder's own top, the bot cannot press its way down, so it keeps walking out over the drop + const float flTopZ = m_ladder->m_top.z; + const bool bOnTop = !m_bGoingUp + && ( myPos.z >= flTopZ || ( mover->IsOnGround() && myPos.z > flTopZ - mover->GetStepHeight() ) ); + + // Within mount range and on the ladder: start climbing + if ( range < MOUNT_RANGE && me->IsOnLadder() && !bOnTop ) + { + 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" ); + } + + // Are we aligned and close enough to mount the ladder? Going down, line up only from in front of the + // face (at an angle the bot ends up on the floor beside the drop); from elsewhere walk straight on + const bool bAligned = dot < ALIGN_DOT_THRESHOLD; + const bool bLineUp = m_bGoingUp ? range >= MOUNT_RANGE + : ( dot < 0.0f && ( range >= MOUNT_RANGE || !bAligned ) ); + if ( bLineUp ) { // Perpendicular alignment line Vector2D alignNormal = ladderNormal2D; @@ -132,44 +229,165 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float ) NDebugOverlay::Cross3D( goal, 5.0f, 255, 0, 255, true, 0.1f ); } } + else if ( m_bGoingUp ? bAligned : !bOnTop ) + { + // Aligned (or going down and off the top), push forward to attach to the ladder + me->PressForwardButton(); + mover->Approach( targetPos ); + } else { - // Within mount range - check if aligned to start climbing - bool onLadder = me->IsOnLadder(); - if ( onLadder ) - { - if ( me->IsDebugging( NEXTBOT_PATH ) ) - { - DevMsg( "%s: Starting ladder climb\n", me->GetDebugIdentifier() ); - } + // Close but not aligned (or going down and still on the top) - continue approaching + mover->Approach( targetPos ); + } - // 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 ) + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +// Going down a ladder whose top stands on a barrier the bot reaches from behind the ladder's plane: +// +// hang point +// face -> x |#| <- parapet, handrail or wall cap, level with the ladder's top +// |#|_______________________ floor the descent starts from +// | <- bot backs out this way +// +// Walking off the top forward, the bot falls past the face moving away from it, which LadderMove() never grabs. +// So it faces the face, hops the barrier and backs out, and the locomotion presses into the face as it drops. +ActionResult CNEOBotLadderApproach::UpdateOverTop( CNEOBot *me, const Vector &hangPos, float range ) +{ + CNEOBotLocomotion *mover = me->GetLocomotionInterface(); + const Vector &feet = mover->GetFeet(); + + // Caught below the top, hanging in front of the face: climb down. Standing on the ladder's own top + // the engine may hold the bot too, but that is no mount: the locomotion lets go of it + if ( me->IsOnLadder() && !mover->IsOnGround() && feet.z < m_ladder->m_top.z && range < MOUNT_RANGE ) + { + mover->StopCatchingLadder(); + me->SetAbsVelocity( vec3_origin ); + return ChangeTo( new CNEOBotLadderClimb( m_ladder, m_bGoingUp ), "Mounting ladder over its top" ); + } + + // Face the ladder from far behind it, just below level: forward then points into the face and reaches furthest, + // and the back press points a little up, never into the ladder's own top + const Vector intoFace = -m_ladder->GetNormal(); + Vector lookAt = m_ladder->m_top + FACE_LOOK_RANGE * intoFace; + lookAt.z = me->EyePosition().z - FACE_LOOK_DROP; + me->GetBodyInterface()->AimHeadTowards( lookAt, IBody::MANDATORY, 0.1f, nullptr, "Facing the ladder to back onto it" ); + + mover->CatchLadderBelowTop( m_ladder ); + + // In the air, hopping onto the barrier or dropping past the face: keep backing out + if ( !mover->IsOnGround() ) + { + me->PressBackwardButton(); + return Continue(); + } + + // How far to the side of the ladder's middle the bot is, and how far out in front of its face + const Vector2D toFeet = ( feet - m_ladder->m_top ).AsVector2D(); + const float side = toFeet.x * intoFace.y - toFeet.y * intoFace.x; + const float out = -DotProduct2D( toFeet, intoFace.AsVector2D() ); + + // A level top can still have something the bot cannot cross between the floor and the face, + // so a bot held behind the face goes back to the ordinary approach + if ( m_bFlushTop && out < 0.0f && m_progressTimer.IsGreaterThen( STALL_TIME ) ) + { + m_bOverTop = false; + mover->StopCatchingLadder(); + m_progressTimer.Start(); + return Continue(); + } + + const float halfHull = me->GetBodyInterface()->GetHullWidth() * 0.5f; + const float rise = m_ladder->m_top.z - feet.z; + const bool bOnTop = rise <= mover->GetStepHeight(); + + if ( fabsf( side ) > MAX( m_ladder->m_width * 0.5f, LINEUP_TOLERANCE ) ) + { + // Line up behind the ladder first: hopping or dropping from beside it, the bot comes down beside it. Near + // the edge, sideways only: a press with any part into the face dips into the ladder's top, which grabs the bot + const bool bNearEdge = bOnTop && out > -( halfHull + EDGE_ZONE ); + const float depth = bNearEdge ? -out : halfHull + ( bOnTop ? 0.0f : LINEUP_DEPTH ); + Vector lineUp = m_ladder->m_top + intoFace * depth; + lineUp.z = feet.z; + mover->Approach( lineUp ); + return Continue(); + } + + if ( !bOnTop ) + { + // On the floor behind the barrier: hop onto it from a standstill, which goes straight up, and back out + // over it. No sidestep: square to the face, backing out heads straight for the hang point + if ( !mover->IsClimbingOrJumping() && rise <= mover->GetMaxJumpHeight() && IsBarrierAhead( me, hangPos ) ) { - // Aligned (or going down), push forward to attach to the ladder - me->PressForwardButton(); - mover->Approach( targetPos ); + me->SetAbsVelocity( vec3_origin ); + mover->Jump(); } - else + + me->PressBackwardButton(); + return Continue(); + } + + // On the barrier's top: back out crouched, so the bot leaves the top slowly and drops within reach of the face + me->PressCrouchButton( CROUCH_HOLD ); + + // Over the ladder's own top, a back press with the view still pointing up (coming down from a climb) + // points into that top: the engine grabs the bot there and slides it off at climbing speed + if ( out > -( halfHull + EDGE_ZONE ) && me->EyeAngles().x < 0.0f ) + { + return Continue(); + } + + // Sidesteps only while the bot's middle is behind the face: one still held as it drops would skew the + // press into the face + if ( out < 0.0f ) + { + mover->Approach( hangPos ); + } + else + { + me->PressBackwardButton(); + + // Backing out over a level top can wedge the bot on the ladder brush's top in front of the face, + // so hop back off it + if ( m_bFlushTop && !mover->IsClimbingOrJumping() && m_progressTimer.IsGreaterThen( STALL_TIME ) ) { - // Close but not aligned - continue approaching to align - mover->Approach( targetPos ); + mover->Jump(); } } return Continue(); } +//--------------------------------------------------------------------------------------------- +// Is the way to the hang point blocked just ahead, a step above the feet: a barrier, not a curb +bool CNEOBotLadderApproach::IsBarrierAhead( CNEOBot *me, const Vector &hangPos ) const +{ + const ILocomotion *mover = me->GetLocomotionInterface(); + Vector toHang = hangPos - mover->GetFeet(); + toHang.z = 0.0f; + if ( toHang.NormalizeInPlace() <= 0.0f ) + { + return false; + } + + const Vector start = mover->GetFeet() + Vector( 0.0f, 0.0f, mover->GetStepHeight() ); + const Vector end = start + BARRIER_PROBE * toHang; + + trace_t tr; + UTIL_TraceHull( start, end, me->WorldAlignMins(), me->WorldAlignMaxs(), + MASK_PLAYERSOLID, me, COLLISION_GROUP_PLAYER_MOVEMENT, &tr ); + + return tr.DidHit(); +} + //--------------------------------------------------------------------------------------------- void CNEOBotLadderApproach::OnEnd( CNEOBot *me, Action *nextAction ) { me->StartLookingAroundForEnemies(); me->ClearAttribute( CNEOBot::IGNORE_ENEMIES ); + me->GetLocomotionInterface()->StopCatchingLadder(); if ( me->IsDebugging( NEXTBOT_PATH ) ) { 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 aa52154b73..14ef92d057 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 @@ -29,11 +29,30 @@ class CNEOBotLadderApproach : public Action static constexpr float ALIGN_RANGE = 100.0f; // Distance to start alignment behavior private: + ActionResult UpdateOverTop( CNEOBot *me, const Vector &hangPos, float range ); + bool IsBarrierAhead( CNEOBot *me, const Vector &hangPos ) const; + const CNavLadder *m_ladder; bool m_bGoingUp; + bool m_bOverTop; // going down from behind a barrier at the ladder's top (UpdateOverTop) + bool m_bFlushTop; // over the top with no barrier: the floor is level with it + Vector m_vecLastProgressPos; + IntervalTimer m_progressTimer; // since the bot last moved PROGRESS_STEP Vector m_ladderCenter; CountdownTimer m_timeoutTimer; static constexpr float MOUNT_RANGE = 25.0f; // Distance to start climbing + static constexpr float HANG_CLEARANCE = 2.0f; // Gap between the face and the hull of a bot about to mount in front of it static constexpr float ALIGN_DOT_THRESHOLD = -0.9f; // cos(~25 degrees) alignment tolerance + + static constexpr float OVER_TOP_TIMEOUT = 5.0f; // approach timeout going over the top: line up, hop, back out + static constexpr float FACE_LOOK_RANGE = 1000.0f; // how far into the face the view is pointed going over the top + static constexpr float FACE_LOOK_DROP = 50.0f; // and how far below eye level there (3 degrees) + static constexpr float LINEUP_TOLERANCE = 6.0f; // least distance to the side of the ladder's middle a hop may start + static constexpr float LINEUP_DEPTH = 8.0f; // the line-up point on the floor, behind the face beyond the hull's half width + static constexpr float BARRIER_PROBE = 4.0f; // how close a barrier on the way to the hang point is hopped onto + static constexpr float EDGE_ZONE = 8.0f; // beyond the hull's half width behind the face: over the ladder's own top + static constexpr float CROUCH_HOLD = 0.3f; // crouch held from each update while backing out on the top + static constexpr float PROGRESS_STEP = 4.0f; // a move this far counts as progress + static constexpr float STALL_TIME = 0.6f; // and none for this long, over a level top, means blocked (measured on ridgeline L1) }; 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 0ce5ad60ea..09a5b22501 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 @@ -7,12 +7,19 @@ // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" +// The engine grabs a ladder within this distance of its face (CGameMovement::LadderDistance()) +static constexpr float LADDER_GRAB_DIST = 2.0f; +// A hull placed off a ladder starts this far clear of the face, +static constexpr float LADDER_PLACE_GAP = 2.0f; +// and is slid at most this far towards the face to find it. +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_bDismountPhase( false ), m_bJumpedOffLadder( false ), m_pExitArea( nullptr ), m_nNudges( 0 ) { - m_exitAreaCenter = vec3_origin; + m_dismountPos = vec3_origin; m_ladderForward = ladder ? -ladder->GetNormal() : vec3_origin; } @@ -68,23 +75,59 @@ ActionResult CNEOBotLadderClimb::OnStart( CNEOBot *me, Action mover->Reset(); // clear velocity cache in locomotion interface me->SetAbsVelocity( vec3_origin ); + // Half a hull off the nav line, unchecked: the fallback spot when the clear spot below starts in solid 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; + float offsetDist = me->CollisionProp()->OBBSize().x / 2.0f + LADDER_PLACE_GAP; idealPos += m_ladder->GetNormal() * offsetDist; idealPos.z = m_flLastZ; + // On a ladder turned off the world axes, half a hull width off the face leaves the hull's corner in the brush, + // so clear the corners first, then slide in until the hull touches the face + const Vector &normal = m_ladder->GetNormal(); + const float flReach = me->CollisionProp()->OBBSize().x / 2.0f * ( fabsf( normal.x ) + fabsf( normal.y ) ); + Vector clearPos = m_ladder->GetPosAtHeight( m_flLastZ ) + normal * ( flReach + LADDER_PLACE_GAP ); + clearPos.z = m_flLastZ; + + trace_t trFace; + UTIL_TraceHull( clearPos, clearPos - normal * LADDER_PLACE_SEEK, me->WorldAlignMins(), me->WorldAlignMaxs(), + MASK_PLAYERSOLID, me, COLLISION_GROUP_PLAYER_MOVEMENT, &trFace ); + if ( !trFace.startsolid ) + { + idealPos = trFace.DidHit() ? trFace.endpos + normal * ( LADDER_GRAB_DIST * 0.5f ) : clearPos; + } + + // The floor at the spot can stand higher than the feet were, and a hull sunk into a displacement does not start solid, + // so stand the hull on that floor: unducking later on the ladder lowers the feet by the crouch's lift, into the floor + trace_t trFloor; + UTIL_TraceHull( idealPos + Vector( 0.0f, 0.0f, mover->GetStepHeight() ), idealPos, me->WorldAlignMins(), me->WorldAlignMaxs(), + MASK_PLAYERSOLID, me, COLLISION_GROUP_PLAYER_MOVEMENT, &trFloor ); + if ( !trFloor.startsolid && trFloor.DidHit() ) + { + idealPos.z = trFloor.endpos.z; + } + // 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; + // A spot still in solid, such as inside a ladder brush that stands in front of its nav line, would hold the bot + // there for good, so the bot climbs from where it is instead + trace_t trSpot; + UTIL_TraceHull( idealPos, idealPos, me->WorldAlignMins(), me->WorldAlignMaxs(), + MASK_PLAYERSOLID, me, COLLISION_GROUP_PLAYER_MOVEMENT, &trSpot ); + if ( !trSpot.startsolid ) + { + // Teleport the bot + me->SetAbsOrigin( idealPos ); + me->SetAbsAngles( idealAngles ); + // the view too: a forward press with the view off the face's normal slides the bot off the side of a narrow ladder + me->SnapEyeAngles( idealAngles ); + + // Update mover feet to new teleported position for stuck checking + m_flLastZ = idealPos.z; + } } else if ( me->IsDebugging( NEXTBOT_PATH ) ) { @@ -103,9 +146,44 @@ ActionResult CNEOBotLadderClimb::OnStart( CNEOBot *me, Action // Try to resolve the exit area from the current path early ResolveExitArea( me ); + ClaimLadder( me ); + return Continue(); } +//--------------------------------------------------------------------------------------------- +// PlayerLocomotion lets go of a ladder it was never asked to use, and once the contact persists +// takes it over by the nearer end - at the foot, the bottom. Tell it this climb is wanted. +void CNEOBotLadderClimb::ClaimLadder( CNEOBot *me ) const +{ + ILocomotion *mover = me->GetLocomotionInterface(); + if ( mover->IsUsingLadder() ) + { + return; + } + + const CNavArea *pExitArea = m_pExitArea; + if ( !pExitArea ) + { + pExitArea = LadderEndArea(); + } + + // The locomotion's dismount walks to this area, so there is nothing to claim with without one + if ( !pExitArea ) + { + return; + } + + if ( m_bGoingUp ) + { + mover->ClimbLadder( m_ladder, pExitArea ); + } + else + { + mover->DescendLadder( m_ladder, pExitArea ); + } +} + //--------------------------------------------------------------------------------------------- void CNEOBotLadderClimb::ResolveExitArea( CNEOBot *me ) { @@ -122,6 +200,20 @@ void CNEOBotLadderClimb::ResolveExitArea( CNEOBot *me ) } constexpr int MAX_PATH_SEARCH_STEPS = 100; + + // The path's current goal can still be the area at the ladder's foot when the climb reaches the + // top, and that area is no exit: the exit is the first area after this ladder + const Path::Segment *ladderSeg = seg; + for ( int i = 0; ladderSeg && ladderSeg->ladder != m_ladder && i < MAX_PATH_SEARCH_STEPS; ++i ) + { + ladderSeg = path->NextSegment( ladderSeg ); + } + + if ( ladderSeg && ladderSeg->ladder == m_ladder ) + { + seg = ladderSeg; + } + int safetyCounter = 0; while ( seg && safetyCounter < MAX_PATH_SEARCH_STEPS ) { @@ -137,10 +229,76 @@ void CNEOBotLadderClimb::ResolveExitArea( CNEOBot *me ) if ( seg && !seg->ladder && seg->area ) { - m_pExitArea = seg->area; - m_exitAreaCenter = m_pExitArea->GetCenter(); + m_pExitArea = FindLanding( path, seg->area ); + m_dismountPos = GetDismountPos( me ); + } + } +} + +//--------------------------------------------------------------------------------------------- +// By the dismount the path's goal can already be past the area this ladder lands on, +// so take a landing the path passes through, or else the ladder's own end area, or, with no end area, the path's +const CNavArea *CNEOBotLadderClimb::FindLanding( const PathFollower *path, const CNavArea *pathExit ) const +{ + const CNavLadder::LadderDirectionType dir = m_bGoingUp ? CNavLadder::LADDER_UP : CNavLadder::LADDER_DOWN; + if ( m_ladder->IsConnected( pathExit, dir ) ) + { + return pathExit; + } + + for ( const Path::Segment *seg = path->FirstSegment(); seg; seg = path->NextSegment( seg ) ) + { + if ( seg->area && m_ladder->IsConnected( seg->area, dir ) ) + { + return seg->area; } } + + const CNavArea *ladderEnd = LadderEndArea(); + return ladderEnd ? ladderEnd : pathExit; +} + +//--------------------------------------------------------------------------------------------- +// The area at the end this climb leaves the ladder by +const CNavArea *CNEOBotLadderClimb::LadderEndArea() const +{ + return m_bGoingUp ? m_ladder->GetTopArea() : m_ladder->m_bottomArea; +} + +//--------------------------------------------------------------------------------------------- +// A landing whose short side is under half a hull is a wall cap the climb crosses, not a floor to stand on, +// and a deeper one holds the bot's center even where the path follower's LadderCapAhead() needs the mesh link to climb onto it +bool CNEOBotLadderClimb::IsNarrowLanding( CNEOBot *me ) const +{ + const float flMinDepth = me->GetBodyInterface()->GetHullWidth() * NARROW_LANDING_HULLS; + return Min( m_pExitArea->GetSizeX(), m_pExitArea->GetSizeY() ) < flMinDepth; +} + +//--------------------------------------------------------------------------------------------- +// Step off toward where the landing meets this end of the ladder: a long, narrow landing's center +// can lie off to one side, and the dismount kick toward it carries the bot past the landing's edge +Vector CNEOBotLadderClimb::GetDismountPos( CNEOBot *me ) const +{ + if ( IsNarrowLanding( me ) ) + { + return m_pExitArea->GetCenter(); + } + + const Vector &ladderEnd = m_bGoingUp ? m_ladder->m_top : m_ladder->m_bottom; + Vector nearPoint; + m_pExitArea->GetClosestPointOnArea( ladderEnd, &nearPoint ); + + // into the wall at the top, where the landing is, and away from it at the bottom + Vector inward = m_bGoingUp ? m_ladderForward : -m_ladderForward; + inward.z = 0.0f; + if ( inward.NormalizeInPlace() > 0.0f ) + { + nearPoint += inward * LANDING_INSET; + } + + Vector dismountPos; + m_pExitArea->GetClosestPointOnArea( nearPoint, &dismountPos ); + return dismountPos; } @@ -175,7 +333,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* if ( m_pExitArea ) { - toExit = m_exitAreaCenter - myPos; + toExit = m_dismountPos - myPos; toExit.z = 0.0f; if ( toExit.Length2DSqr() > 0.01f ) { @@ -218,9 +376,23 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* //------------------------------------------------------------ if ( !m_bDismountPhase ) { + // The locomotion drops its claim whenever the engine lets go of the bot, even for a tick + if ( me->GetMoveType() == MOVETYPE_LADDER ) + { + ClaimLadder( me ); + } + float currentZ = myPos.z; float targetZ = m_bGoingUp ? m_ladder->m_top.z : m_ladder->m_bottom.z; + // Going down, the bot holds still until its view makes forward take it down: not a stall + const bool bHoldForView = !m_bGoingUp && !me->GetLocomotionInterface()->IsForwardDownLadder( m_ladder ); + if ( bHoldForView ) + { + m_flLastZ = currentZ; + m_stuckTimer.Start( STUCK_CHECK_INTERVAL ); + } + // Stuck detection: if we haven't made vertical progress, bail out gracefully if ( m_stuckTimer.IsElapsed() ) { @@ -234,6 +406,13 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* EnterDismountPhase( me ); return Continue(); } + else if ( m_nNudges < STALL_NUDGES ) + { + // Usually snagged on something at the ladder's edge, not blocked outright: + // shimmy sideways, right then left, until the next progress check, before giving up + ++m_nNudges; + m_nudgeTimer.Start( STUCK_CHECK_INTERVAL ); + } else { // We are stuck mid-climb. Reset scenario by jumping backwards and ending condition @@ -251,13 +430,15 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* m_stuckTimer.Start( STUCK_CHECK_INTERVAL ); } - // Early jump-off + // Early jump-off. Going down, only once a standing height below the top: from any higher, the + // kick towards the exit lands the bot back on the floor the descent started from. bool bWantsDismount = false; if ( m_pExitArea ) { - float zDistToExit = currentZ - m_exitAreaCenter.z; + float zDistToExit = currentZ - m_dismountPos.z; + const bool bBelowTopFloor = m_bGoingUp || currentZ < m_ladder->m_top.z - body->GetStandHullHeight(); - if ( zDistToExit > 0.0f && zDistToExit <= SAFE_FALL_DIST ) + if ( zDistToExit > 0.0f && zDistToExit <= SAFE_FALL_DIST && bBelowTopFloor ) { bWantsDismount = true; } @@ -275,13 +456,13 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* { if ( bExitIsBehind ) { - dismountZ = Min( m_exitAreaCenter.z, targetZ ); + dismountZ = Min( m_dismountPos.z, targetZ ); } } else { // Allow early drop-off at intermediate floors - dismountZ = Max( m_exitAreaCenter.z, targetZ ); + dismountZ = Max( m_dismountPos.z, targetZ ); } } @@ -309,7 +490,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* bool bShouldGoUp = m_bGoingUp; if ( m_pExitArea ) { - bShouldGoUp = ( currentZ < m_exitAreaCenter.z ); + bShouldGoUp = ( currentZ < m_dismountPos.z ); } if ( bLipInTheWay ) @@ -329,12 +510,37 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* } - // 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); + // A descent the locomotion has claimed is aimed by its DescendLadder(); + // a second MANDATORY aim here would hold the view back from it + if ( m_bGoingUp || !mover->IsUsingLadder() ) + { + // 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)" ); + } + + if ( bHoldForView ) + { + me->ReleaseForwardButton(); + } + else + { + me->PressForwardButton(0.1f); + } + + if ( onLadder && m_nudgeTimer.HasStarted() && !m_nudgeTimer.IsElapsed() ) + { + if ( m_nNudges % 2 ) + { + me->PressRightButton( 0.1f ); + } + else + { + me->PressLeftButton( 0.1f ); + } + } } //------------------------------------------------------------ @@ -342,8 +548,10 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* //------------------------------------------------------------ if ( m_bDismountPhase ) { - // Reached the target NavArea after the ladder - if ( m_pExitArea && me->GetLastKnownArea() == m_pExitArea ) + // The last known area becomes the landing while the bot is still in the air beside it, + // so finish once the bot stands on the landing, or enters a narrow one it crosses + if ( m_pExitArea && me->GetLastKnownArea() == m_pExitArea + && ( IsNarrowLanding( me ) || ( mover->IsOnGround() && m_pExitArea->IsOverlapping( myPos ) ) ) ) { return Done( "Reached next NavArea after dismount" ); } @@ -357,11 +565,11 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* // Build look target toward exit area center with vertical bias preserved if ( m_pExitArea ) { - bool bDroppingEarly = ( myPos.z >= m_exitAreaCenter.z ); + bool bDroppingEarly = ( myPos.z >= m_dismountPos.z ); // Maintain Z-height while on the ladder in the dismount phase if ( onLadder ) { - if ( myPos.z < m_exitAreaCenter.z ) + if ( myPos.z < m_dismountPos.z ) { me->PressMoveUpButton(); } @@ -375,7 +583,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* bDroppingEarly = true; } - body->AimHeadTowards( m_exitAreaCenter, IBody::MANDATORY, 0.1f, nullptr, "Walking to exit area" ); + body->AimHeadTowards( m_dismountPos, 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 ); @@ -408,7 +616,7 @@ ActionResult CNEOBotLadderClimb::Update( CNEOBot *me, float /*interval* if ( me->IsDebugging( NEXTBOT_PATH ) ) { - NDebugOverlay::Line( myPos, m_exitAreaCenter, 0, 255, 255, true, 0.1f ); + NDebugOverlay::Line( myPos, m_dismountPos, 0, 255, 255, true, 0.1f ); } } else 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 d1b34e2917..f95510f443 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 @@ -4,6 +4,7 @@ #include "bot/neo_bot.h" class CNavLadder; +class PathFollower; //---------------------------------------------------------------------------------------------------------------- /** @@ -29,7 +30,12 @@ class CNEOBotLadderClimb : public Action private: void EnterDismountPhase( CNEOBot *me ); void ResolveExitArea( CNEOBot *me ); + const CNavArea *FindLanding( const PathFollower *path, const CNavArea *pathExit ) const; + const CNavArea *LadderEndArea() const; + bool IsNarrowLanding( CNEOBot *me ) const; + Vector GetDismountPos( CNEOBot *me ) const; bool IsDismountBlocked( CNEOBot *me, const Vector &toExit ) const; + void ClaimLadder( CNEOBot *me ) const; const CNavLadder *m_ladder; const CNavArea *m_pExitArea; @@ -39,12 +45,14 @@ class CNEOBotLadderClimb : public Action bool m_bJumpedOffLadder; float m_flLastZ; - Vector m_exitAreaCenter; + Vector m_dismountPos; Vector m_ladderForward; CountdownTimer m_timeoutTimer; CountdownTimer m_stuckTimer; CountdownTimer m_dismountTimer; + CountdownTimer m_nudgeTimer; // shimmying sideways past a snag + int m_nNudges; static constexpr float STUCK_CHECK_INTERVAL = 0.4f; static constexpr float STUCK_Z_TOLERANCE = 2.0f; @@ -52,4 +60,7 @@ class CNEOBotLadderClimb : public Action 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 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 int STALL_NUDGES = 3; // Sideways shimmies before a stalled climb jumps off (measured on subsurface L1 and saitama L2) };