You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
527 lines
15 KiB
527 lines
15 KiB
//========= 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; |
|
} |
|
|
|
//=============================================================================
|
|
|