528 lines
16 KiB
C++
528 lines
16 KiB
C++
//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
|
|
//
|
|
// Purpose:
|
|
//
|
|
// $NoKeywords: $
|
|
//=============================================================================//
|
|
|
|
|
|
#include "cbase.h"
|
|
|
|
#include "game.h"
|
|
#include "ndebugoverlay.h"
|
|
|
|
#include "ai_basenpc.h"
|
|
#include "ai_hull.h"
|
|
#include "ai_node.h"
|
|
#include "ai_motor.h"
|
|
#include "ai_navigator.h"
|
|
#include "ai_hint.h"
|
|
#include "scripted.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
//=============================================================================
|
|
// PATHING & HIGHER LEVEL MOVEMENT
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Static debug function to force all selected npcs to go to the
|
|
// given node
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
void CAI_BaseNPC::ForceSelectedGo(CBaseEntity *pPlayer, const Vector &targetPos, const Vector &traceDir, bool bRun)
|
|
{
|
|
CAI_BaseNPC *npc = gEntList.NextEntByClass( (CAI_BaseNPC *)NULL );
|
|
for ( ; npc; npc = gEntList.NextEntByClass(npc) )
|
|
{
|
|
if ( ( npc->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT ) == 0 )
|
|
continue;
|
|
|
|
// If a behavior is active, we need to stop running it
|
|
npc->SetPrimaryBehavior( NULL );
|
|
|
|
Vector chasePosition = targetPos;
|
|
npc->TranslateNavGoal( pPlayer, chasePosition );
|
|
// It it legal to drop me here
|
|
Vector vUpBit = chasePosition;
|
|
vUpBit.z += 1;
|
|
|
|
trace_t tr;
|
|
AI_TraceHull( chasePosition, vUpBit, npc->GetHullMins(),
|
|
npc->GetHullMaxs(), npc->GetAITraceMask(), npc, COLLISION_GROUP_NONE, &tr );
|
|
if (tr.startsolid || tr.fraction != 1.0 )
|
|
{
|
|
NDebugOverlay::BoxAngles(chasePosition, npc->GetHullMins(),
|
|
npc->GetHullMaxs(), npc->GetAbsAngles(), 255,0,0,20,0.5);
|
|
}
|
|
|
|
npc->m_vecLastPosition = chasePosition;
|
|
|
|
if (npc->m_hCine != NULL)
|
|
{
|
|
npc->ExitScriptedSequence();
|
|
}
|
|
|
|
npc->SetSchedule( bRun ? SCHED_FORCED_GO_RUN : SCHED_FORCED_GO );
|
|
npc->m_flMoveWaitFinished = gpGlobals->curtime;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Static debug function to make all selected npcs run around
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
void CAI_BaseNPC::ForceSelectedGoRandom(void)
|
|
{
|
|
CAI_BaseNPC *npc = gEntList.NextEntByClass( (CAI_BaseNPC *)NULL );
|
|
|
|
while (npc)
|
|
{
|
|
if (npc->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT)
|
|
{
|
|
// If a behavior is active, we need to stop running it
|
|
npc->SetPrimaryBehavior( NULL );
|
|
npc->SetSchedule( SCHED_RUN_RANDOM );
|
|
npc->GetNavigator()->SetMovementActivity(ACT_RUN);
|
|
}
|
|
npc = gEntList.NextEntByClass(npc);
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
bool CAI_BaseNPC::ScheduledMoveToGoalEntity( int scheduleType, CBaseEntity *pGoalEntity, Activity movementActivity )
|
|
{
|
|
// If a behavior is active, we need to stop running it
|
|
SetPrimaryBehavior( NULL );
|
|
|
|
if ( m_NPCState == NPC_STATE_NONE )
|
|
{
|
|
// More than likely being grabbed before first think. Set ideal state to prevent schedule stomp
|
|
m_NPCState = m_IdealNPCState;
|
|
}
|
|
|
|
SetSchedule( scheduleType );
|
|
|
|
SetGoalEnt( pGoalEntity );
|
|
|
|
// HACKHACK: Call through TranslateNavGoal to fixup this goal position
|
|
// UNDONE: Remove this and have NPCs that need this functionality fix up paths in the
|
|
// movement system instead of when they are specified.
|
|
AI_NavGoal_t goal(pGoalEntity->GetAbsOrigin(), movementActivity, AIN_DEF_TOLERANCE, AIN_YAW_TO_DEST);
|
|
|
|
TranslateNavGoal( pGoalEntity, goal.dest );
|
|
|
|
return GetNavigator()->SetGoal( goal );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
bool CAI_BaseNPC::ScheduledFollowPath( int scheduleType, CBaseEntity *pPathStart, Activity movementActivity )
|
|
{
|
|
// If a behavior is active, we need to stop running it
|
|
SetPrimaryBehavior( NULL );
|
|
|
|
if ( m_NPCState == NPC_STATE_NONE )
|
|
{
|
|
// More than likely being grabbed before first think. Set ideal state to prevent schedule stomp
|
|
m_NPCState = m_IdealNPCState;
|
|
}
|
|
|
|
SetSchedule( scheduleType );
|
|
|
|
SetGoalEnt( pPathStart );
|
|
|
|
// HACKHACK: Call through TranslateNavGoal to fixup this goal position
|
|
AI_NavGoal_t goal(GOALTYPE_PATHCORNER, pPathStart->GetLocalOrigin(), movementActivity, AIN_DEF_TOLERANCE, AIN_YAW_TO_DEST);
|
|
|
|
TranslateNavGoal( pPathStart, goal.dest );
|
|
|
|
return GetNavigator()->SetGoal( goal );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
bool CAI_BaseNPC::IsMoving( void )
|
|
{
|
|
return GetNavigator()->IsGoalSet();
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
bool CAI_BaseNPC::IsCurTaskContinuousMove()
|
|
{
|
|
const Task_t* pTask = GetTask();
|
|
|
|
// This bit of logic strikes me funny, but the case does exist. (sjb)
|
|
if( !pTask )
|
|
return true;
|
|
|
|
switch( pTask->iTask )
|
|
{
|
|
case TASK_WAIT_FOR_MOVEMENT:
|
|
case TASK_MOVE_TO_TARGET_RANGE:
|
|
case TASK_MOVE_TO_GOAL_RANGE:
|
|
case TASK_WEAPON_RUN_PATH:
|
|
case TASK_PLAY_SCENE:
|
|
case TASK_RUN_PATH_TIMED:
|
|
case TASK_WALK_PATH_TIMED:
|
|
case TASK_RUN_PATH_FOR_UNITS:
|
|
case TASK_WALK_PATH_FOR_UNITS:
|
|
case TASK_RUN_PATH_FLEE:
|
|
case TASK_WALK_PATH_WITHIN_DIST:
|
|
case TASK_RUN_PATH_WITHIN_DIST:
|
|
return true;
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Used to specify that the NPC has a reason not to use the a navigation node
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
bool CAI_BaseNPC::IsUnusableNode(int iNodeID, CAI_Hint *pHint)
|
|
{
|
|
if ( m_bHintGroupNavLimiting && m_strHintGroup != NULL_STRING && STRING(m_strHintGroup)[0] != 0 )
|
|
{
|
|
if (!pHint || pHint->GetGroup() != GetHintGroup())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Checks the validity of the given route's goaltype
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
bool CAI_BaseNPC::ValidateNavGoal()
|
|
{
|
|
if (GetNavigator()->GetGoalType() == GOALTYPE_COVER)
|
|
{
|
|
// Check if this location will block my enemy's line of sight to me
|
|
if (GetEnemy())
|
|
{
|
|
Activity nCoverActivity = GetCoverActivity( GetHintNode() );
|
|
Vector vCoverLocation = GetNavigator()->GetGoalPos();
|
|
|
|
|
|
// For now we have to drop the node to the floor so we can
|
|
// get an accurate postion of the NPC. Should change once Ken checks in
|
|
float floorZ = GetFloorZ(vCoverLocation);
|
|
vCoverLocation.z = floorZ;
|
|
|
|
|
|
Vector vEyePos = vCoverLocation + EyeOffset(nCoverActivity);
|
|
|
|
if (!IsCoverPosition( GetEnemy()->EyePosition(), vEyePos ) )
|
|
{
|
|
TaskFail(FAIL_BAD_PATH_GOAL);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
float CAI_BaseNPC::OpenDoorAndWait( CBaseEntity *pDoor )
|
|
{
|
|
float flTravelTime = 0;
|
|
|
|
//DevMsg( 2, "A door. ");
|
|
if (pDoor && !pDoor->IsLockedByMaster())
|
|
{
|
|
pDoor->Use(this, this, USE_ON, 0.0);
|
|
flTravelTime = pDoor->GetMoveDoneTime();
|
|
if ( pDoor->GetEntityName() != NULL_STRING )
|
|
{
|
|
CBaseEntity *pTarget = NULL;
|
|
for (;;)
|
|
{
|
|
pTarget = gEntList.FindEntityByName( pTarget, pDoor->GetEntityName() );
|
|
|
|
if ( pTarget != pDoor )
|
|
{
|
|
if ( !pTarget )
|
|
break;
|
|
|
|
if ( FClassnameIs( pTarget, pDoor->GetClassname() ) )
|
|
{
|
|
pTarget->Use(this, this, USE_ON, 0.0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return gpGlobals->curtime + flTravelTime;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
bool CAI_BaseNPC::CanStandOn( CBaseEntity *pSurface ) const
|
|
{
|
|
if ( !pSurface->IsAIWalkable() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
CAI_Navigator *pNavigator = const_cast<CAI_Navigator *>(GetNavigator());
|
|
|
|
if ( pNavigator->IsGoalActive() &&
|
|
pSurface == pNavigator->GetGoalTarget() )
|
|
return false;
|
|
|
|
return BaseClass::CanStandOn( pSurface );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
bool CAI_BaseNPC::IsJumpLegal( const Vector &startPos, const Vector &apex, const Vector &endPos,
|
|
float maxUp, float maxDown, float maxDist ) const
|
|
{
|
|
if ((endPos.z - startPos.z) > maxUp + 0.1)
|
|
return false;
|
|
if ((startPos.z - endPos.z) > maxDown + 0.1)
|
|
return false;
|
|
|
|
if ((apex.z - startPos.z) > maxUp * 1.25 )
|
|
return false;
|
|
|
|
float dist = (startPos - endPos).Length();
|
|
if ( dist > maxDist + 0.1)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Returns true if a reasonable jumping distance
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
bool CAI_BaseNPC::IsJumpLegal( const Vector &startPos, const Vector &apex, const Vector &endPos ) const
|
|
{
|
|
const float MAX_JUMP_RISE = 80.0f;
|
|
const float MAX_JUMP_DISTANCE = 250.0f;
|
|
const float MAX_JUMP_DROP = 192.0f;
|
|
|
|
return IsJumpLegal( startPos, apex, endPos, MAX_JUMP_RISE, MAX_JUMP_DROP, MAX_JUMP_DISTANCE );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Returns a throw velocity from start to end position
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
Vector CAI_BaseNPC::CalcThrowVelocity(const Vector &startPos, const Vector &endPos, float fGravity, float fArcSize)
|
|
{
|
|
// Get the height I have to throw to get to the target
|
|
float stepHeight = endPos.z - startPos.z;
|
|
float throwHeight = 0;
|
|
|
|
// -----------------------------------------------------------------
|
|
// Now calcluate the distance to a point halfway between our current
|
|
// and target position. (the apex of our throwing arc)
|
|
// -----------------------------------------------------------------
|
|
Vector targetDir2D = endPos - startPos;
|
|
targetDir2D.z = 0;
|
|
|
|
float distance = VectorNormalize(targetDir2D);
|
|
|
|
|
|
// If jumping up we want to throw a bit higher than the height diff
|
|
if (stepHeight > 0)
|
|
{
|
|
throwHeight = stepHeight + fArcSize;
|
|
}
|
|
else
|
|
{
|
|
throwHeight = fArcSize;
|
|
}
|
|
// Make sure that I at least catch some air
|
|
if (throwHeight < fArcSize)
|
|
{
|
|
throwHeight = fArcSize;
|
|
}
|
|
|
|
|
|
// -------------------------------------------------------------
|
|
// calculate the vertical and horizontal launch velocities
|
|
// -------------------------------------------------------------
|
|
float velVert = (float)sqrt(2.0f*fGravity*throwHeight);
|
|
|
|
float divisor = velVert;
|
|
divisor += (float)sqrt((2.0f*(-fGravity)*(stepHeight-throwHeight)));
|
|
|
|
float velHorz = (distance * fGravity)/divisor;
|
|
|
|
// -----------------------------------------------------------
|
|
// Make the horizontal throw vector and add vertical component
|
|
// -----------------------------------------------------------
|
|
Vector throwVel = targetDir2D * velHorz;
|
|
throwVel.z = velVert;
|
|
|
|
return throwVel;
|
|
}
|
|
|
|
bool CAI_BaseNPC::ShouldMoveWait()
|
|
{
|
|
return (m_flMoveWaitFinished > gpGlobals->curtime);
|
|
}
|
|
|
|
float CAI_BaseNPC::GetStepDownMultiplier() const
|
|
{
|
|
return m_pNavigator->GetStepDownMultiplier();
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: execute any movement this sequence may have
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
bool CAI_BaseNPC::AutoMovement( CBaseEntity *pTarget, AIMoveTrace_t *pTraceResult )
|
|
{
|
|
return AutoMovement( GetAnimTimeInterval(), pTarget, pTraceResult );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : flInterval -
|
|
// -
|
|
// *pTraceResult -
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CAI_BaseNPC::AutoMovement( float flInterval, CBaseEntity *pTarget, AIMoveTrace_t *pTraceResult )
|
|
{
|
|
bool ignored;
|
|
Vector newPos;
|
|
QAngle newAngles;
|
|
|
|
if (flInterval <= 0.0)
|
|
return true;
|
|
|
|
m_ScheduleState.bTaskRanAutomovement = true;
|
|
|
|
if (GetIntervalMovement( flInterval, ignored, newPos, newAngles ))
|
|
{
|
|
// DevMsg( "%.2f : (%.1f) %.1f %.1f %.1f\n", gpGlobals->curtime, (newPos - GetLocalOrigin()).Length(), newPos.x, newPos.y, newAngles.y );
|
|
|
|
if ( m_hCine )
|
|
{
|
|
m_hCine->ModifyScriptedAutoMovement( &newPos );
|
|
}
|
|
|
|
if (GetMoveType() == MOVETYPE_STEP)
|
|
{
|
|
if (!(GetFlags() & FL_FLY))
|
|
{
|
|
if ( !pTarget )
|
|
{
|
|
pTarget = GetNavTargetEntity();
|
|
}
|
|
|
|
// allow NPCs to adjust the automatic movement
|
|
if ( ModifyAutoMovement( newPos ) )
|
|
{
|
|
// Set our motor's speed here
|
|
Vector vecOriginalPosition = GetAbsOrigin();
|
|
bool bResult = false;
|
|
if (!TaskIsComplete())
|
|
{
|
|
bResult = ( GetMotor()->MoveGroundStep( newPos, pTarget, newAngles.y, false, true, pTraceResult ) == AIM_SUCCESS );
|
|
}
|
|
|
|
Vector change = GetAbsOrigin() - vecOriginalPosition;
|
|
if (flInterval != 0)
|
|
{
|
|
change /= flInterval;
|
|
}
|
|
|
|
GetMotor()->SetMoveVel(change);
|
|
|
|
return bResult;
|
|
}
|
|
|
|
return ( GetMotor()->MoveGroundStep( newPos, pTarget, newAngles.y, false, true, pTraceResult ) == AIM_SUCCESS );
|
|
}
|
|
else
|
|
{
|
|
// FIXME: here's no direct interface to a fly motor, plus this needs to support a state where going through the world is okay.
|
|
// FIXME: add callbacks into the script system for validation
|
|
// FIXME: add function on scripts to force only legal movements
|
|
// FIXME: GetIntervalMovement deals in Local space, nor global. Currently now way to communicate that through these interfaces.
|
|
SetLocalOrigin( newPos );
|
|
SetLocalAngles( newAngles );
|
|
return true;
|
|
}
|
|
}
|
|
else if (GetMoveType() == MOVETYPE_FLY)
|
|
{
|
|
Vector dist = newPos - GetLocalOrigin();
|
|
|
|
VectorScale( dist, 1.0 / flInterval, dist );
|
|
|
|
SetLocalVelocity( dist );
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: return max 1/10 second rate of turning
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
|
|
float CAI_BaseNPC::MaxYawSpeed( void )
|
|
{
|
|
return 45;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Returns the estimate in seconds before we reach our nav goal.
|
|
// -1 means we don't know / haven't calculated it yet.
|
|
//-----------------------------------------------------------------------------
|
|
float CAI_BaseNPC::GetTimeToNavGoal()
|
|
{
|
|
float flDist = GetNavigator()->BuildAndGetPathDistToGoal();
|
|
if ( flDist < 0 )
|
|
{
|
|
return -1.0f;
|
|
}
|
|
|
|
float flSpeed = GetIdealSpeed();
|
|
|
|
// FIXME: needs to consider stopping time!
|
|
if (flSpeed > 0 && flDist > 0)
|
|
{
|
|
return flDist / flSpeed;
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
//=============================================================================
|