diff --git a/src/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp b/src/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp index 012a7afdbc..3ef0fb1f8d 100644 --- a/src/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp +++ b/src/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp @@ -17,6 +17,30 @@ ConVar NextBotPlayerMoveDirect( "nb_player_move_direct", "0" ); +// NEO: CGameMovement::LadderMove() puts a player on any ladder their wish direction points at, +// while TraverseLadder()'s NO_LADDER branch drops the move type straight back to walking whenever +// the bot's own state machine did not ask for a climb. A path that merely brushes past a ladder +// leaves the two toggling every tick: the bot hangs on the ladder making no progress, and because +// it never stops moving, no stuck event fires either. +// +// Letting go harder does not fix it - the wish direction still points at the brush. What does is +// giving up the argument and riding the ladder out by its nearer end, but only for the contacts +// that actually persist; most sort themselves out within a second and adopting those buys a lot of +// climbing nobody asked for. + +// How long a bot must be held against a ladder it did not ask for before taking it over. Short +// enough that a real snag is caught within half a second, long enough that the ordinary release +// still handles the many contacts that clear themselves immediately. +static const float LADDER_ADOPT_TIME = 0.5f; +// A ladder further than this from the bot is not the one it is stuck on. +static const 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 const float LADDER_CONTACT_RESET = 1.0f; + +// NEO: going down a ladder, forward is pressed only once it moves the bot down at least this +// fraction of the climb speed. Slower than that, the view is still coming round. +static const float LADDER_MIN_DESCENT_RATE = 0.25f; + //----------------------------------------------------------------------------------------------------- PlayerLocomotion::PlayerLocomotion( INextBot *bot ) : ILocomotion( bot ) { @@ -45,6 +69,9 @@ void PlayerLocomotion::Reset( void ) m_ladderDismountGoal = NULL; m_ladderTimer.Invalidate(); + m_unwantedLadderSince = 0.0f; + m_unwantedLadderLastTouch = 0.0f; + m_minSpeedLimit = 0.0f; m_maxSpeedLimit = 9999999.9f; @@ -52,6 +79,140 @@ void PlayerLocomotion::Reset( void ) } +//----------------------------------------------------------------------------------------------------- +/** + * 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 hull reaches a standing height above the feet, and the grab only needs the hull to touch + // the brush - so a bot standing on the floor well *below* a ladder whose foot hangs over a + // walkway is exactly the case to catch here, not one 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; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * NEO: 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 ) +{ + const CNavLadder *ladder = FindTouchedLadder(); + if ( ladder == NULL ) + { + return false; + } + + // Most grabs sort themselves out within a second, and adopting every one of them buys a lot of + // climbing nobody asked for. Only take the ladder over once the bot has actually 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; + } + + // Leave by the nearer end rather than argue about being here at all. Whichever end the bot is + // closer to is the one it can reach soonest, and the dismount goal has to be a real area or + // DismountLadderTop/Bottom has nothing to walk to. + // + // Climbing out upward only was measured and is worse: it sends the bot somewhere it then has to + // come back from, and costs captures even though it reads better on the ladder numbers. + const bool bGoUp = ( GetFeet().z - ladder->m_bottom.z ) > ( ladder->m_top.z - GetFeet().z ); + + // A ladder's top is recorded in whichever of the three slots the generator filled, and plenty + // of them leave m_topForwardArea empty - reading only that slot silently skips those ladders. + const CNavArea *dismount = ladder->m_bottomArea; + + if ( bGoUp ) + { + dismount = ladder->m_topForwardArea ? ladder->m_topForwardArea : + ( ladder->m_topLeftArea ? ladder->m_topLeftArea : ladder->m_topRightArea ); + } + + if ( dismount == NULL ) + { + return false; + } + + m_ladderInfo = ladder; + m_ladderDismountGoal = dismount; + m_ladderState = bGoUp ? ASCENDING_LADDER : DESCENDING_LADDER; + + return true; +} + + +//----------------------------------------------------------------------------------------------------- +// NEO: 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; +} + + +//----------------------------------------------------------------------------------------------------- +// NEO: 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; + } + + // The vertical speed a forward press gives, as a fraction of the climb speed + const float climbRate = view.z + intoFace; + + return climbRate < -LADDER_MIN_DESCENT_RATE; +} + + //----------------------------------------------------------------------------------------------------- bool PlayerLocomotion::TraverseLadder( void ) { @@ -87,7 +248,13 @@ bool PlayerLocomotion::TraverseLadder( void ) if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER ) { - // on ladder and don't want to be + // On a ladder and don't want to be. HandleUnwantedLadder() may take the ladder over + // instead, in which case the state machine drives from here and we are done. + if ( HandleUnwantedLadder() ) + { + break; + } + GetBot()->GetEntity()->SetMoveType( MOVETYPE_WALK ); } return false; @@ -115,6 +282,13 @@ PlayerLocomotion::LadderState PlayerLocomotion::ApproachAscendingLadder( void ) return DISMOUNTING_LADDER_TOP; } + // 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; + } + // sanity check - are we too far below this ladder to reach it? if ( GetFeet().z <= m_ladderInfo->m_bottom.z - GetMaxJumpHeight() ) { @@ -149,6 +323,13 @@ PlayerLocomotion::LadderState PlayerLocomotion::ApproachDescendingLadder( void ) return NO_LADDER; } + // 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; + } + // sanity check - are we already at the end of this ladder? if ( GetFeet().z <= m_ladderInfo->m_bottom.z + GetMaxJumpHeight() ) { @@ -280,8 +461,9 @@ PlayerLocomotion::LadderState PlayerLocomotion::DescendLadder( void ) return DISMOUNTING_LADDER_BOTTOM; } - // climb down this ladder - look down - Vector goal = GetFeet() + 100.0f * ( m_ladderInfo->GetNormal() + Vector( 0, 0, -2 ) ); + // NEO: climb down this ladder - look down 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 ) ); GetBot()->GetBodyInterface()->AimHeadTowards( goal, IBody::MANDATORY, 0.1f, NULL, "Ladder" ); @@ -489,7 +671,12 @@ void PlayerLocomotion::Approach( const Vector &pos, float goalWeight ) } #endif - if ( m_player->IsOnLadder() && IsUsingLadder() && ( m_ladderState == ASCENDING_LADDER || m_ladderState == DESCENDING_LADDER ) ) + if ( m_player->IsOnLadder() && m_ladderState == DESCENDING_LADDER && m_ladderInfo && !IsForwardDownLadder( m_ladderInfo ) ) + { + // 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. + } + else if ( 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(); diff --git a/src/game/server/NextBot/Player/NextBotPlayerLocomotion.h b/src/game/server/NextBot/Player/NextBotPlayerLocomotion.h index 6a947835ca..92235864bb 100644 --- a/src/game/server/NextBot/Player/NextBotPlayerLocomotion.h +++ b/src/game/server/NextBot/Player/NextBotPlayerLocomotion.h @@ -61,6 +61,7 @@ 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; + bool IsForwardDownLadder( const CNavLadder *ladder ) const; // NEO: would pressing forward move us down this ladder virtual void FaceTowards( const Vector &target ); // rotate body to face towards "target" @@ -127,6 +128,15 @@ class PlayerLocomotion : public ILocomotion const CNavArea *m_ladderDismountGoal; CountdownTimer m_ladderTimer; // a "give up" timer if things go awry + // NEO: a ladder the engine put us on that our own path never asked for. Releasing it is not + // enough on its own - CGameMovement::LadderMove() re-grabs from the bot's wish direction, so + // the two toggle each other until something breaks the loop. See TraverseLadder(). + bool HandleUnwantedLadder( void ); + const CNavLadder *FindTouchedLadder( void ) const; + Vector GetIntoLadderFace( const CNavLadder *ladder ) const; // NEO: 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 + bool IsClimbPossible( INextBot *me, const CBaseEntity *obstacle ) const; }; 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..31259a19da 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 @@ -86,6 +86,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(); @@ -99,6 +105,14 @@ ActionResult CNEOBotLadderApproach::Update( CNEOBot *me, float ) // 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 +121,33 @@ 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 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,34 +171,16 @@ 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() ); - } - - // 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 - mover->Approach( targetPos ); - } + // Close but not aligned (or going down and still on the top) - continue approaching + mover->Approach( targetPos ); } return Continue(); 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..49fcd0a220 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 @@ -35,5 +35,6 @@ class CNEOBotLadderApproach : public Action 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 a descending bot's hull as it mounts 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 0ce5ad60ea..5a9fd3ffb8 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 @@ -103,9 +103,51 @@ 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 = m_bGoingUp ? m_ladder->m_topForwardArea : m_ladder->m_bottomArea; + } + + // The generator fills whichever top slot the landing is in, often not the forward one + if ( !pExitArea && m_bGoingUp ) + { + pExitArea = m_ladder->m_topLeftArea ? m_ladder->m_topLeftArea : + ( m_ladder->m_topRightArea ? m_ladder->m_topRightArea : m_ladder->m_topBehindArea ); + } + + // 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 ) { @@ -218,9 +260,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() ) { @@ -251,13 +307,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; + 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; } @@ -329,12 +387,25 @@ 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); + } } //------------------------------------------------------------ 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..e60ff5e514 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 @@ -30,6 +30,7 @@ class CNEOBotLadderClimb : public Action void EnterDismountPhase( CNEOBot *me ); void ResolveExitArea( CNEOBot *me ); bool IsDismountBlocked( CNEOBot *me, const Vector &toExit ) const; + void ClaimLadder( CNEOBot *me ) const; const CNavLadder *m_ladder; const CNavArea *m_pExitArea;