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.
1834 lines
46 KiB
1834 lines
46 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
//=============================================================================// |
|
|
|
// Author: Michael S. Booth (mike@turtlerockstudios.com), 2003 |
|
|
|
#include "cbase.h" |
|
#include "cs_bot.h" |
|
#include "datacache/imdlcache.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Used to update view angles to stay on a ladder |
|
*/ |
|
inline float StayOnLadderLine( CCSBot *me, const CNavLadder *ladder ) |
|
{ |
|
// determine our facing |
|
NavDirType faceDir = AngleToDirection( me->EyeAngles().y ); |
|
|
|
const float stiffness = 1.0f; |
|
|
|
// move toward ladder mount point |
|
switch( faceDir ) |
|
{ |
|
case NORTH: |
|
return stiffness * (ladder->m_top.x - me->GetAbsOrigin().x); |
|
|
|
case SOUTH: |
|
return -stiffness * (ladder->m_top.x - me->GetAbsOrigin().x); |
|
|
|
case WEST: |
|
return -stiffness * (ladder->m_top.y - me->GetAbsOrigin().y); |
|
|
|
case EAST: |
|
return stiffness * (ladder->m_top.y - me->GetAbsOrigin().y); |
|
} |
|
|
|
return 0.0f; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
void CCSBot::ComputeLadderAngles( float *yaw, float *pitch ) |
|
{ |
|
if ( !yaw || !pitch ) |
|
return; |
|
|
|
Vector myOrigin = GetCentroid( this ); |
|
|
|
// set yaw to aim at ladder |
|
Vector to = m_pathLadder->GetPosAtHeight(myOrigin.z) - myOrigin; |
|
float idealYaw = UTIL_VecToYaw( to ); |
|
|
|
Vector faceDir = (m_pathLadderFaceIn) ? -m_pathLadder->GetNormal() : m_pathLadder->GetNormal(); |
|
QAngle faceAngles; |
|
VectorAngles( faceDir, faceAngles ); |
|
|
|
const float lookAlongLadderRange = 50.0f; |
|
const float ladderPitchUpApproach = -30.0f; |
|
const float ladderPitchUpTraverse = -60.0f; // -80 |
|
const float ladderPitchDownApproach = 0.0f; |
|
const float ladderPitchDownTraverse = 80.0f; |
|
|
|
// adjust pitch to look up/down ladder as we ascend/descend |
|
switch( m_pathLadderState ) |
|
{ |
|
case APPROACH_ASCENDING_LADDER: |
|
{ |
|
Vector to = m_goalPosition - myOrigin; |
|
*yaw = idealYaw; |
|
|
|
if (to.IsLengthLessThan( lookAlongLadderRange )) |
|
*pitch = ladderPitchUpApproach; |
|
break; |
|
} |
|
|
|
case APPROACH_DESCENDING_LADDER: |
|
{ |
|
Vector to = m_goalPosition - myOrigin; |
|
*yaw = idealYaw; |
|
|
|
if (to.IsLengthLessThan( lookAlongLadderRange )) |
|
*pitch = ladderPitchDownApproach; |
|
break; |
|
} |
|
|
|
case FACE_ASCENDING_LADDER: |
|
if ( m_pathLadderDismountDir == LEFT ) |
|
{ |
|
*yaw = AngleNormalizePositive( idealYaw + 90.0f ); |
|
} |
|
else if ( m_pathLadderDismountDir == RIGHT ) |
|
{ |
|
*yaw = AngleNormalizePositive( idealYaw - 90.0f ); |
|
} |
|
else |
|
{ |
|
*yaw = idealYaw; |
|
} |
|
*pitch = ladderPitchUpApproach; |
|
break; |
|
|
|
case FACE_DESCENDING_LADDER: |
|
*yaw = idealYaw; |
|
*pitch = ladderPitchDownApproach; |
|
break; |
|
|
|
case MOUNT_ASCENDING_LADDER: |
|
case ASCEND_LADDER: |
|
if ( m_pathLadderDismountDir == LEFT ) |
|
{ |
|
*yaw = AngleNormalizePositive( idealYaw + 90.0f ); |
|
} |
|
else if ( m_pathLadderDismountDir == RIGHT ) |
|
{ |
|
*yaw = AngleNormalizePositive( idealYaw - 90.0f ); |
|
} |
|
else |
|
{ |
|
*yaw = faceAngles[ YAW ] + StayOnLadderLine( this, m_pathLadder ); |
|
} |
|
*pitch = ( m_pathLadderState == ASCEND_LADDER ) ? ladderPitchUpTraverse : ladderPitchUpApproach; |
|
break; |
|
|
|
case MOUNT_DESCENDING_LADDER: |
|
case DESCEND_LADDER: |
|
*yaw = faceAngles[ YAW ] + StayOnLadderLine( this, m_pathLadder ); |
|
*pitch = ( m_pathLadderState == DESCEND_LADDER ) ? ladderPitchDownTraverse : ladderPitchDownApproach; |
|
break; |
|
|
|
case DISMOUNT_ASCENDING_LADDER: |
|
if ( m_pathLadderDismountDir == LEFT ) |
|
{ |
|
*yaw = AngleNormalizePositive( faceAngles[ YAW ] + 90.0f ); |
|
} |
|
else if ( m_pathLadderDismountDir == RIGHT ) |
|
{ |
|
*yaw = AngleNormalizePositive( faceAngles[ YAW ] - 90.0f ); |
|
} |
|
else |
|
{ |
|
*yaw = faceAngles[ YAW ]; |
|
} |
|
break; |
|
|
|
case DISMOUNT_DESCENDING_LADDER: |
|
*yaw = faceAngles[ YAW ]; |
|
break; |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Move actual view angles towards desired ones. |
|
* This is the only place v_angle is altered. |
|
* @todo Make stiffness and turn rate constants timestep invariant. |
|
*/ |
|
void CCSBot::UpdateLookAngles( void ) |
|
{ |
|
VPROF_BUDGET( "CCSBot::UpdateLookAngles", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
const float deltaT = g_BotUpkeepInterval; |
|
float maxAccel; |
|
float stiffness; |
|
float damping; |
|
|
|
// If mimicing the player, don't modify the view angles. |
|
if ( bot_mimic.GetInt() ) |
|
return; |
|
|
|
// springs are stiffer when attacking, so we can track and move between targets better |
|
if (IsAttacking()) |
|
{ |
|
stiffness = 300.0f; |
|
damping = 30.0f; // 20 |
|
maxAccel = 3000.0f; // 4000 |
|
} |
|
else |
|
{ |
|
stiffness = 200.0f; |
|
damping = 25.0f; |
|
maxAccel = 3000.0f; |
|
} |
|
|
|
// these may be overridden by ladder logic |
|
float useYaw = m_lookYaw; |
|
float usePitch = m_lookPitch; |
|
|
|
// |
|
// Ladders require precise movement, therefore we need to look at the |
|
// ladder as we approach and ascend/descend it. |
|
// If we are on a ladder, we need to look up or down to traverse it - override pitch in this case. |
|
// |
|
// If we're trying to break something, though, we actually need to look at it before we can |
|
// look at the ladder |
|
// |
|
if ( IsUsingLadder() && !(IsLookingAtSpot( PRIORITY_HIGH ) && m_lookAtSpotAttack) ) |
|
{ |
|
ComputeLadderAngles( &useYaw, &usePitch ); |
|
} |
|
|
|
// get current view angles |
|
QAngle viewAngles = EyeAngles(); |
|
|
|
// |
|
// Yaw |
|
// |
|
float angleDiff = AngleNormalize( useYaw - viewAngles.y ); |
|
|
|
/* |
|
* m_forwardAngle is unreliable. Need to simulate mouse sliding & centering |
|
if (!IsAttacking()) |
|
{ |
|
// do not allow rotation through our reverse facing angle - go the "long" way instead |
|
float toCurrent = AngleNormalize( pev->v_angle.y - m_forwardAngle ); |
|
float toDesired = AngleNormalize( useYaw - m_forwardAngle ); |
|
|
|
// if angle differences are different signs, they cross the forward facing |
|
if (toCurrent * toDesired < 0.0f) |
|
{ |
|
// if the sum of the angles is greater than 180, turn the "long" way around |
|
if (abs( toCurrent - toDesired ) >= 180.0f) |
|
{ |
|
if (angleDiff > 0.0f) |
|
angleDiff -= 360.0f; |
|
else |
|
angleDiff += 360.0f; |
|
} |
|
} |
|
} |
|
*/ |
|
|
|
// if almost at target angle, snap to it |
|
const float onTargetTolerance = 1.0f; // 3 |
|
if (angleDiff < onTargetTolerance && angleDiff > -onTargetTolerance) |
|
{ |
|
m_lookYawVel = 0.0f; |
|
viewAngles.y = useYaw; |
|
} |
|
else |
|
{ |
|
// simple angular spring/damper |
|
float accel = stiffness * angleDiff - damping * m_lookYawVel; |
|
|
|
// limit rate |
|
if (accel > maxAccel) |
|
accel = maxAccel; |
|
else if (accel < -maxAccel) |
|
accel = -maxAccel; |
|
|
|
m_lookYawVel += deltaT * accel; |
|
viewAngles.y += deltaT * m_lookYawVel; |
|
|
|
// keep track of how long our view remains steady |
|
const float steadyYaw = 1000.0f; |
|
if (fabs( accel ) > steadyYaw) |
|
{ |
|
m_viewSteadyTimer.Start(); |
|
} |
|
} |
|
|
|
// |
|
// Pitch |
|
// Actually, this is negative pitch. |
|
// |
|
angleDiff = usePitch - viewAngles.x; |
|
|
|
angleDiff = AngleNormalize( angleDiff ); |
|
|
|
|
|
if (false && angleDiff < onTargetTolerance && angleDiff > -onTargetTolerance) |
|
{ |
|
m_lookPitchVel = 0.0f; |
|
viewAngles.x = usePitch; |
|
} |
|
else |
|
{ |
|
// simple angular spring/damper |
|
// double the stiffness since pitch is only +/- 90 and yaw is +/- 180 |
|
float accel = 2.0f * stiffness * angleDiff - damping * m_lookPitchVel; |
|
|
|
// limit rate |
|
if (accel > maxAccel) |
|
accel = maxAccel; |
|
else if (accel < -maxAccel) |
|
accel = -maxAccel; |
|
|
|
m_lookPitchVel += deltaT * accel; |
|
viewAngles.x += deltaT * m_lookPitchVel; |
|
|
|
// keep track of how long our view remains steady |
|
const float steadyPitch = 1000.0f; |
|
if (fabs( accel ) > steadyPitch) |
|
{ |
|
m_viewSteadyTimer.Start(); |
|
} |
|
} |
|
|
|
//PrintIfWatched( "yawVel = %g, pitchVel = %g\n", m_lookYawVel, m_lookPitchVel ); |
|
|
|
// limit range - avoid gimbal lock |
|
if (viewAngles.x < -89.0f) |
|
viewAngles.x = -89.0f; |
|
else if (viewAngles.x > 89.0f) |
|
viewAngles.x = 89.0f; |
|
|
|
// update view angles |
|
SnapEyeAngles( viewAngles ); |
|
|
|
// if our weapon is zooming, our view is not steady |
|
if (IsWaitingForZoom()) |
|
{ |
|
m_viewSteadyTimer.Start(); |
|
} |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if we can see the point |
|
*/ |
|
bool CCSBot::IsVisible( const Vector &pos, bool testFOV, const CBaseEntity *ignore ) const |
|
{ |
|
VPROF_BUDGET( "CCSBot::IsVisible( pos )", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
// we can't see anything if we're blind |
|
if (IsBlind()) |
|
return false; |
|
|
|
// is it in my general viewcone? |
|
if (testFOV && !(const_cast<CCSBot *>(this)->FInViewCone( pos ))) |
|
return false; |
|
|
|
// check line of sight against smoke |
|
if (TheCSBots()->IsLineBlockedBySmoke( EyePositionConst(), pos )) |
|
return false; |
|
|
|
// check line of sight |
|
// Must include CONTENTS_MONSTER to pick up all non-brush objects like barrels |
|
trace_t result; |
|
CTraceFilterNoNPCsOrPlayer traceFilter( ignore, COLLISION_GROUP_NONE ); |
|
UTIL_TraceLine( EyePositionConst(), pos, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); |
|
if (result.fraction != 1.0f) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if we can see any part of the player |
|
* Check parts in order of importance. Return the first part seen in "visPart" if it is non-NULL. |
|
*/ |
|
bool CCSBot::IsVisible( CCSPlayer *player, bool testFOV, unsigned char *visParts ) const |
|
{ |
|
VPROF_BUDGET( "CCSBot::IsVisible( player )", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
// optimization - assume if center is not in FOV, nothing is |
|
// we're using WorldSpaceCenter instead of GUT so we can skip GetPartPosition below - that's |
|
// the most expensive part of this, and if we can skip it, so much the better. |
|
if (testFOV && !(const_cast<CCSBot *>(this)->FInViewCone( player->WorldSpaceCenter() ))) |
|
{ |
|
return false; |
|
} |
|
|
|
unsigned char testVisParts = NONE; |
|
|
|
// check gut |
|
Vector partPos = GetPartPosition( player, GUT ); |
|
|
|
// finish gut check |
|
if (IsVisible( partPos, testFOV )) |
|
{ |
|
if (visParts == NULL) |
|
return true; |
|
|
|
testVisParts |= GUT; |
|
} |
|
|
|
|
|
// check top of head |
|
partPos = GetPartPosition( player, HEAD ); |
|
if (IsVisible( partPos, testFOV )) |
|
{ |
|
if (visParts == NULL) |
|
return true; |
|
|
|
testVisParts |= HEAD; |
|
} |
|
|
|
// check feet |
|
partPos = GetPartPosition( player, FEET ); |
|
if (IsVisible( partPos, testFOV )) |
|
{ |
|
if (visParts == NULL) |
|
return true; |
|
|
|
testVisParts |= FEET; |
|
} |
|
|
|
// check "edges" |
|
partPos = GetPartPosition( player, LEFT_SIDE ); |
|
if (IsVisible( partPos, testFOV )) |
|
{ |
|
if (visParts == NULL) |
|
return true; |
|
|
|
testVisParts |= LEFT_SIDE; |
|
} |
|
|
|
partPos = GetPartPosition( player, RIGHT_SIDE ); |
|
if (IsVisible( partPos, testFOV )) |
|
{ |
|
if (visParts == NULL) |
|
return true; |
|
|
|
testVisParts |= RIGHT_SIDE; |
|
} |
|
|
|
if (visParts) |
|
*visParts = testVisParts; |
|
|
|
if (testVisParts) |
|
return true; |
|
|
|
return false; |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Interesting part positions |
|
*/ |
|
CCSBot::PartInfo CCSBot::m_partInfo[ MAX_PLAYERS ]; |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Compute part positions from bone location. |
|
*/ |
|
void CCSBot::ComputePartPositions( CCSPlayer *player ) |
|
{ |
|
const int headBox = 12; |
|
const int gutBox = 9; |
|
const int leftElbowBox = 14; |
|
const int rightElbowBox = 17; |
|
//const int hipBox = 0; |
|
//const int leftFootBox = 4; |
|
//const int rightFootBox = 8; |
|
const int maxBoxIndex = rightElbowBox; |
|
|
|
VPROF_BUDGET( "CCSBot::ComputePartPositions", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
// which PartInfo corresponds to the given player |
|
PartInfo *info = &m_partInfo[ player->entindex() % MAX_PLAYERS ]; |
|
|
|
// always compute feet, since it doesn't rely on bones |
|
info->m_feetPos = player->GetAbsOrigin(); |
|
info->m_feetPos.z += 5.0f; |
|
|
|
// get bone positions for interesting points on the player |
|
MDLCACHE_CRITICAL_SECTION(); |
|
CStudioHdr *studioHdr = player->GetModelPtr(); |
|
if (studioHdr) |
|
{ |
|
mstudiohitboxset_t *set = studioHdr->pHitboxSet( player->GetHitboxSet() ); |
|
if (set && maxBoxIndex < set->numhitboxes) |
|
{ |
|
QAngle angles; |
|
mstudiobbox_t *box; |
|
|
|
// gut |
|
box = set->pHitbox( gutBox ); |
|
player->GetBonePosition( box->bone, info->m_gutPos, angles ); |
|
|
|
// head |
|
box = set->pHitbox( headBox ); |
|
player->GetBonePosition( box->bone, info->m_headPos, angles ); |
|
|
|
Vector forward, right; |
|
AngleVectors( angles, &forward, &right, NULL ); |
|
|
|
// in local bone space |
|
const float headForwardOffset = 4.0f; |
|
const float headRightOffset = 2.0f; |
|
info->m_headPos += headForwardOffset * forward + headRightOffset * right; |
|
|
|
/// @todo Fix this hack - lower the head target because it's a bit too high for the current T model |
|
info->m_headPos.z -= 2.0f; |
|
|
|
|
|
// left side |
|
box = set->pHitbox( leftElbowBox ); |
|
player->GetBonePosition( box->bone, info->m_leftSidePos, angles ); |
|
|
|
// right side |
|
box = set->pHitbox( rightElbowBox ); |
|
player->GetBonePosition( box->bone, info->m_rightSidePos, angles ); |
|
|
|
return; |
|
} |
|
} |
|
|
|
|
|
// default values if bones are not available |
|
info->m_headPos = GetCentroid( player ); |
|
info->m_gutPos = info->m_headPos; |
|
info->m_leftSidePos = info->m_headPos; |
|
info->m_rightSidePos = info->m_headPos; |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return world space position of given part on player. |
|
* Uses hitboxes to get accurate positions. |
|
* @todo Optimize by computing once for each player and storing. |
|
*/ |
|
const Vector &CCSBot::GetPartPosition( CCSPlayer *player, VisiblePartType part ) const |
|
{ |
|
VPROF_BUDGET( "CCSBot::GetPartPosition", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
// which PartInfo corresponds to the given player |
|
PartInfo *info = &m_partInfo[ player->entindex() % MAX_PLAYERS ]; |
|
|
|
if (gpGlobals->framecount > info->m_validFrame) |
|
{ |
|
// update part positions |
|
const_cast< CCSBot * >( this )->ComputePartPositions( player ); |
|
info->m_validFrame = gpGlobals->framecount; |
|
} |
|
|
|
// return requested part position |
|
switch( part ) |
|
{ |
|
default: |
|
{ |
|
AssertMsg( false, "GetPartPosition: Invalid part" ); |
|
// fall thru to GUT |
|
} |
|
|
|
case GUT: |
|
return info->m_gutPos; |
|
|
|
case HEAD: |
|
return info->m_headPos; |
|
|
|
case FEET: |
|
return info->m_feetPos; |
|
|
|
case LEFT_SIDE: |
|
return info->m_leftSidePos; |
|
|
|
case RIGHT_SIDE: |
|
return info->m_rightSidePos; |
|
} |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Update desired view angles to point towards m_lookAtSpot |
|
*/ |
|
void CCSBot::UpdateLookAt( void ) |
|
{ |
|
Vector to = m_lookAtSpot - EyePositionConst(); |
|
|
|
QAngle idealAngle; |
|
VectorAngles( to, idealAngle ); |
|
|
|
//Vector idealAngle = UTIL_VecToAngles( to ); |
|
//idealAngle.x = 360.0f - idealAngle.x; |
|
|
|
SetLookAngles( idealAngle.y, idealAngle.x ); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Look at the given point in space for the given duration (-1 means forever) |
|
*/ |
|
void CCSBot::SetLookAt( const char *desc, const Vector &pos, PriorityType pri, float duration, bool clearIfClose, float angleTolerance, bool attack ) |
|
{ |
|
if (IsBlind()) |
|
return; |
|
|
|
// if currently looking at a point in space with higher priority, ignore this request |
|
if (m_lookAtSpotState != NOT_LOOKING_AT_SPOT && m_lookAtSpotPriority > pri) |
|
return; |
|
|
|
// if already looking at this spot, just extend the time |
|
const float tolerance = 10.0f; |
|
if (m_lookAtSpotState != NOT_LOOKING_AT_SPOT && VectorsAreEqual( pos, m_lookAtSpot, tolerance )) |
|
{ |
|
m_lookAtSpotDuration = duration; |
|
|
|
if (m_lookAtSpotPriority < pri) |
|
m_lookAtSpotPriority = pri; |
|
} |
|
else |
|
{ |
|
// look at new spot |
|
m_lookAtSpot = pos; |
|
m_lookAtSpotState = LOOK_TOWARDS_SPOT; |
|
m_lookAtSpotDuration = duration; |
|
m_lookAtSpotPriority = pri; |
|
} |
|
|
|
m_lookAtSpotAngleTolerance = angleTolerance; |
|
m_lookAtSpotClearIfClose = clearIfClose; |
|
m_lookAtDesc = desc; |
|
m_lookAtSpotAttack = attack; |
|
|
|
PrintIfWatched( "%3.1f SetLookAt( %s ), duration = %f\n", gpGlobals->curtime, desc, duration ); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Block all "look at" and "look around" behavior for given duration - just look ahead |
|
*/ |
|
void CCSBot::InhibitLookAround( float duration ) |
|
{ |
|
m_inhibitLookAroundTimestamp = gpGlobals->curtime + duration; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Update enounter spot timestamps, etc |
|
*/ |
|
void CCSBot::UpdatePeripheralVision() |
|
{ |
|
VPROF_BUDGET( "CCSBot::UpdatePeripheralVision", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
const float peripheralUpdateInterval = 0.29f; // if we update at 10Hz, this ensures we test once every three |
|
if (gpGlobals->curtime - m_peripheralTimestamp < peripheralUpdateInterval) |
|
return; |
|
|
|
m_peripheralTimestamp = gpGlobals->curtime; |
|
|
|
if (m_spotEncounter) |
|
{ |
|
// check LOS to all spots in case we see them with our "peripheral vision" |
|
const SpotOrder *spotOrder; |
|
Vector pos; |
|
|
|
FOR_EACH_VEC( m_spotEncounter->spots, it ) |
|
{ |
|
spotOrder = &m_spotEncounter->spots[ it ]; |
|
|
|
const Vector &spotPos = spotOrder->spot->GetPosition(); |
|
|
|
pos.x = spotPos.x; |
|
pos.y = spotPos.y; |
|
pos.z = spotPos.z + HalfHumanHeight; |
|
|
|
if (!IsVisible( pos, CHECK_FOV )) |
|
continue; |
|
|
|
// can see hiding spot, remember when we saw it last |
|
SetHidingSpotCheckTimestamp( spotOrder->spot ); |
|
} |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Update the "looking around" behavior. |
|
*/ |
|
void CCSBot::UpdateLookAround( bool updateNow ) |
|
{ |
|
VPROF_BUDGET( "CCSBot::UpdateLookAround", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
// |
|
// If we recently saw an enemy, look towards where we last saw them |
|
// Unless we can hear them moving, in which case look towards the noise |
|
// |
|
const float closeRange = 500.0f; |
|
if (!IsNoiseHeard() || GetNoiseRange() > closeRange) |
|
{ |
|
const float recentThreatTime = 1.0f; // 0.25f; |
|
if (!IsLookingAtSpot( PRIORITY_MEDIUM ) && gpGlobals->curtime - m_lastSawEnemyTimestamp < recentThreatTime) |
|
{ |
|
ClearLookAt(); |
|
|
|
Vector spot = m_lastEnemyPosition; |
|
|
|
// find enemy position on the ground |
|
if (TheNavMesh->GetSimpleGroundHeight( m_lastEnemyPosition, &spot.z )) |
|
{ |
|
spot.z += HalfHumanHeight; |
|
SetLookAt( "Last Enemy Position", spot, PRIORITY_MEDIUM, RandomFloat( 2.0f, 3.0f ), true ); |
|
return; |
|
} |
|
} |
|
} |
|
|
|
// |
|
// Look at nearby enemy noises |
|
// |
|
if (UpdateLookAtNoise()) |
|
return; |
|
|
|
|
|
// check if looking around has been inhibited |
|
// Moved inhibit to allow high priority enemy lookats to still occur |
|
if (gpGlobals->curtime < m_inhibitLookAroundTimestamp) |
|
return; |
|
|
|
// |
|
// If we are hiding (or otherwise standing still), watch all approach points leading into this region |
|
// |
|
const float minStillTime = 2.0f; |
|
if (IsAtHidingSpot() || IsNotMoving( minStillTime )) |
|
{ |
|
// update approach points |
|
const float recomputeApproachPointTolerance = 50.0f; |
|
if ((m_approachPointViewPosition - GetAbsOrigin()).IsLengthGreaterThan( recomputeApproachPointTolerance )) |
|
{ |
|
ComputeApproachPoints(); |
|
m_approachPointViewPosition = GetAbsOrigin(); |
|
} |
|
|
|
// if we're sniping, zoom in to watch our approach points |
|
if (IsUsingSniperRifle()) |
|
{ |
|
// low skill bots don't pre-zoom |
|
if (GetProfile()->GetSkill() > 0.4f) |
|
{ |
|
if (!IsViewMoving()) |
|
{ |
|
float range = ComputeWeaponSightRange(); |
|
AdjustZoom( range ); |
|
} |
|
else |
|
{ |
|
// zoom out |
|
if (GetZoomLevel() != NO_ZOOM) |
|
SecondaryAttack(); |
|
} |
|
} |
|
} |
|
|
|
if (m_lastKnownArea == NULL) |
|
return; |
|
|
|
if (gpGlobals->curtime < m_lookAroundStateTimestamp) |
|
return; |
|
|
|
// if we're sniping, switch look-at spots less often |
|
if (IsUsingSniperRifle()) |
|
m_lookAroundStateTimestamp = gpGlobals->curtime + RandomFloat( 5.0f, 10.0f ); |
|
else |
|
m_lookAroundStateTimestamp = gpGlobals->curtime + RandomFloat( 1.0f, 2.0f ); // 0.5, 1.0 |
|
|
|
|
|
#define MAX_APPROACHES 16 |
|
Vector validSpot[ MAX_APPROACHES ]; |
|
int validSpotCount = 0; |
|
|
|
Vector *earlySpot = NULL; |
|
float earliest = 999999.9f; |
|
|
|
for( int i=0; i<m_approachPointCount; ++i ) |
|
{ |
|
float spotTime = m_approachPoint[i].m_area->GetEarliestOccupyTime( OtherTeam( GetTeamNumber() ) ); |
|
|
|
// ignore approach areas the enemy could not have possibly reached yet |
|
if (TheCSBots()->GetElapsedRoundTime() >= spotTime) |
|
{ |
|
validSpot[ validSpotCount++ ] = m_approachPoint[i].m_pos; |
|
} |
|
else |
|
{ |
|
// keep track of earliest spot we can see in case we get there very early |
|
if (spotTime < earliest) |
|
{ |
|
earlySpot = &m_approachPoint[i].m_pos; |
|
earliest = spotTime; |
|
} |
|
} |
|
} |
|
|
|
Vector spot; |
|
|
|
if (validSpotCount) |
|
{ |
|
int which = RandomInt( 0, validSpotCount-1 ); |
|
spot = validSpot[ which ]; |
|
} |
|
else if (earlySpot) |
|
{ |
|
// all of the spots we can see can't be reached yet by the enemy - look at the earliest spot |
|
spot = *earlySpot; |
|
} |
|
else |
|
{ |
|
return; |
|
} |
|
|
|
// don't look at the floor, look roughly at chest level |
|
/// @todo If this approach point is very near, this will cause us to aim up in the air if were crouching |
|
spot.z += HalfHumanHeight; |
|
|
|
SetLookAt( "Approach Point (Hiding)", spot, PRIORITY_LOW ); |
|
|
|
return; |
|
} |
|
|
|
// |
|
// Glance at "encouter spots" as we move past them |
|
// |
|
if (m_spotEncounter) |
|
{ |
|
// |
|
// Check encounter spots |
|
// |
|
if (!IsSafe() && !IsLookingAtSpot( PRIORITY_LOW )) |
|
{ |
|
// allow a short time to look where we're going |
|
if (gpGlobals->curtime < m_spotCheckTimestamp) |
|
return; |
|
|
|
/// @todo Use skill parameter instead of accuracy |
|
|
|
// lower skills have exponentially longer delays |
|
float asleep = (1.0f - GetProfile()->GetSkill()); |
|
asleep *= asleep; |
|
asleep *= asleep; |
|
|
|
m_spotCheckTimestamp = gpGlobals->curtime + asleep * RandomFloat( 10.0f, 30.0f ); |
|
|
|
|
|
// figure out how far along the path segment we are |
|
Vector delta = m_spotEncounter->path.to - m_spotEncounter->path.from; |
|
float length = delta.Length(); |
|
float adx = (float)fabs(delta.x); |
|
float ady = (float)fabs(delta.y); |
|
float t; |
|
Vector myOrigin = GetCentroid( this ); |
|
|
|
if (adx > ady) |
|
t = (myOrigin.x - m_spotEncounter->path.from.x) / delta.x; |
|
else |
|
t = (myOrigin.y - m_spotEncounter->path.from.y) / delta.y; |
|
|
|
// advance parameter a bit so we "lead" our checks |
|
const float leadCheckRange = 50.0f; |
|
t += leadCheckRange / length; |
|
|
|
if (t < 0.0f) |
|
t = 0.0f; |
|
else if (t > 1.0f) |
|
t = 1.0f; |
|
|
|
// collect the unchecked spots so far |
|
#define MAX_DANGER_SPOTS 16 |
|
HidingSpot *dangerSpot[MAX_DANGER_SPOTS]; |
|
int dangerSpotCount = 0; |
|
int dangerIndex = 0; |
|
|
|
const float checkTime = 10.0f; |
|
const SpotOrder *spotOrder; |
|
FOR_EACH_VEC( m_spotEncounter->spots, it ) |
|
{ |
|
spotOrder = &(m_spotEncounter->spots[ it ]); |
|
|
|
// if we have seen this spot recently, we don't need to look at it |
|
if (gpGlobals->curtime - GetHidingSpotCheckTimestamp( spotOrder->spot ) <= checkTime) |
|
continue; |
|
|
|
if (spotOrder->t > t) |
|
break; |
|
|
|
// ignore spots the enemy could not have possibly reached yet |
|
if (spotOrder->spot->GetArea()) |
|
{ |
|
if (TheCSBots()->GetElapsedRoundTime() < spotOrder->spot->GetArea()->GetEarliestOccupyTime( OtherTeam( GetTeamNumber() ) )) |
|
{ |
|
continue; |
|
} |
|
} |
|
|
|
dangerSpot[ dangerIndex++ ] = spotOrder->spot; |
|
if (dangerIndex >= MAX_DANGER_SPOTS) |
|
dangerIndex = 0; |
|
if (dangerSpotCount < MAX_DANGER_SPOTS) |
|
++dangerSpotCount; |
|
} |
|
|
|
if (dangerSpotCount) |
|
{ |
|
// pick one of the spots at random |
|
int which = RandomInt( 0, dangerSpotCount-1 ); |
|
|
|
// glance at the spot for minimum time |
|
SetLookAt( "Encounter Spot", dangerSpot[which]->GetPosition() + Vector( 0, 0, HalfHumanHeight ), PRIORITY_LOW, 0.2f, true, 10.0f ); |
|
|
|
// immediately mark it as "checked", so we don't check it again |
|
// if we get distracted before we check it - that's the way it goes |
|
SetHidingSpotCheckTimestamp( dangerSpot[which] ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* "Bend" our line of sight around corners until we can "see" the point. |
|
*/ |
|
bool CCSBot::BendLineOfSight( const Vector &eye, const Vector &target, Vector *bend, float angleLimit ) const |
|
{ |
|
VPROF_BUDGET( "CCSBot::BendLineOfSight", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
bool doDebug = false; |
|
const float debugDuration = 0.04f; |
|
if (doDebug && cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) |
|
NDebugOverlay::Line( eye, target, 255, 255, 255, true, debugDuration ); |
|
|
|
// if we can directly see the point, use it |
|
trace_t result; |
|
CTraceFilterNoNPCsOrPlayer traceFilter( this, COLLISION_GROUP_NONE ); |
|
UTIL_TraceLine( eye, target, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); |
|
if (result.fraction == 1.0f && !result.startsolid) |
|
{ |
|
// can directly see point, no bending needed |
|
*bend = target; |
|
return true; |
|
} |
|
|
|
// "bend" our line of sight until we can see the approach point |
|
Vector to = target - eye; |
|
float startAngle = UTIL_VecToYaw( to ); |
|
float length = to.Length2D(); |
|
to.NormalizeInPlace(); |
|
|
|
struct Color3 |
|
{ |
|
int r, g, b; |
|
}; |
|
const int colorCount = 6; |
|
Color3 colorSet[ colorCount ] = |
|
{ |
|
{ 255, 0, 0 }, |
|
{ 0, 255, 0 }, |
|
{ 0, 0, 255 }, |
|
{ 255, 255, 0 }, |
|
{ 0, 255, 255 }, |
|
{ 255, 0, 255 }, |
|
}; |
|
|
|
int color = 0; |
|
|
|
// optiming assumption - previous rays cast "shadow" on subsequent rays since they already |
|
// enumerated visible space along their length. |
|
// We should do a dot product and compute the exact length, but since the angular changes |
|
// are incremental, using the direct length should be close enough. |
|
float priorVisibleLength[2] = { 0.0f, 0.0f }; |
|
|
|
float angleInc = 5.0f; |
|
for( float angle = angleInc; angle <= angleLimit; angle += angleInc ) |
|
{ |
|
// check both sides at this angle offset |
|
for( int side=0; side<2; ++side ) |
|
{ |
|
float actualAngle = (side) ? (startAngle + angle) : (startAngle - angle); |
|
|
|
float dx = cos( 3.141592f * actualAngle / 180.0f ); |
|
float dy = sin( 3.141592f * actualAngle / 180.0f ); |
|
|
|
// compute rotated point ray endpoint |
|
Vector rotPoint( eye.x + length * dx, eye.y + length * dy, target.z ); |
|
|
|
// check LOS to find length to test along ray |
|
UTIL_TraceLine( eye, rotPoint, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); |
|
|
|
// if this ray started in an obstacle, skip it |
|
if (result.startsolid) |
|
{ |
|
continue; |
|
} |
|
|
|
Vector ray = rotPoint - eye; |
|
float rayLength = ray.NormalizeInPlace(); |
|
float visibleLength = rayLength * result.fraction; |
|
|
|
if (doDebug && cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) |
|
{ |
|
NDebugOverlay::Line( eye, eye + visibleLength * ray, colorSet[color].r, colorSet[color].g, colorSet[color].b, true, debugDuration ); |
|
} |
|
|
|
// step along ray, checking if point is visible from ray point |
|
const float bendStepSize = 50.0f; |
|
|
|
// start from point that prior rays couldn't see |
|
float startLength = priorVisibleLength[ side ]; |
|
|
|
for( float bendLength=startLength; bendLength <= visibleLength; bendLength += bendStepSize ) |
|
{ |
|
// compute point along ray |
|
Vector bendPoint = eye + bendLength * ray; |
|
|
|
// check if we can see approach point from this bend point |
|
UTIL_TraceLine( bendPoint, target, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); |
|
|
|
if (doDebug && cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) |
|
{ |
|
NDebugOverlay::Line( bendPoint, result.endpos, colorSet[color].r/2, colorSet[color].g/2, colorSet[color].b/2, true, debugDuration ); |
|
} |
|
|
|
if (result.fraction == 1.0f && !result.startsolid) |
|
{ |
|
// target is visible from this bend point on the ray - use this point on the ray as our point |
|
|
|
// keep "bent" point at correct height along line of sight |
|
bendPoint.z = eye.z + bendLength * to.z; |
|
|
|
*bend = bendPoint; |
|
|
|
return true; |
|
} |
|
} |
|
|
|
priorVisibleLength[ side ] = visibleLength; |
|
|
|
++color; |
|
if (color >= colorCount) |
|
{ |
|
color = 0; |
|
} |
|
} // side |
|
} |
|
|
|
// bending rays didn't help - still can't see the point |
|
return false; |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if we "notice" given player |
|
* @todo Increase chance if player is rotating |
|
* @todo Decrease chance as nears edge of FOV |
|
*/ |
|
bool CCSBot::IsNoticable( const CCSPlayer *player, unsigned char visParts ) const |
|
{ |
|
// if this player has just fired his weapon, we notice him |
|
if (DidPlayerJustFireWeapon( player )) |
|
{ |
|
return true; |
|
} |
|
|
|
float deltaT = m_attentionInterval.GetElapsedTime(); |
|
|
|
// all chances are specified in terms of a standard "quantum" of time |
|
// in which a normal person would notice something |
|
const float noticeQuantum = 0.25f; |
|
|
|
// determine percentage of player that is visible |
|
float coverRatio = 0.0f; |
|
|
|
if (visParts & GUT) |
|
{ |
|
const float chance = 40.0f; |
|
coverRatio += chance; |
|
} |
|
|
|
if (visParts & HEAD) |
|
{ |
|
const float chance = 10.0f; |
|
coverRatio += chance; |
|
} |
|
|
|
if (visParts & LEFT_SIDE) |
|
{ |
|
const float chance = 20.0f; |
|
coverRatio += chance; |
|
} |
|
|
|
if (visParts & RIGHT_SIDE) |
|
{ |
|
const float chance = 20.0f; |
|
coverRatio += chance; |
|
} |
|
|
|
if (visParts & FEET) |
|
{ |
|
const float chance = 10.0f; |
|
coverRatio += chance; |
|
} |
|
|
|
|
|
// compute range modifier - farther away players are harder to notice, depeding on what they are doing |
|
float range = (player->GetAbsOrigin() - GetAbsOrigin()).Length(); |
|
const float closeRange = 300.0f; |
|
const float farRange = 1000.0f; |
|
|
|
float rangeModifier; |
|
if (range < closeRange) |
|
{ |
|
rangeModifier = 0.0f; |
|
} |
|
else if (range > farRange) |
|
{ |
|
rangeModifier = 1.0f; |
|
} |
|
else |
|
{ |
|
rangeModifier = (range - closeRange)/(farRange - closeRange); |
|
} |
|
|
|
|
|
// harder to notice when crouched |
|
bool isCrouching = (player->GetFlags() & FL_DUCKING); |
|
|
|
|
|
// moving players are easier to spot |
|
float playerSpeedSq = player->GetAbsVelocity().LengthSqr(); |
|
const float runSpeed = 200.0f; |
|
const float walkSpeed = 30.0f; |
|
float farChance, closeChance; |
|
if (playerSpeedSq > runSpeed * runSpeed) |
|
{ |
|
// running players are always easy to spot (must be standing to run) |
|
return true; |
|
} |
|
else if (playerSpeedSq > walkSpeed * walkSpeed) |
|
{ |
|
// walking players are less noticable far away |
|
if (isCrouching) |
|
{ |
|
closeChance = 90.0f; |
|
farChance = 60.0f; |
|
} |
|
else // standing |
|
{ |
|
closeChance = 100.0f; |
|
farChance = 75.0f; |
|
} |
|
} |
|
else |
|
{ |
|
// motionless players are hard to notice |
|
if (isCrouching) |
|
{ |
|
// crouching and motionless - very tough to notice |
|
closeChance = 80.0f; |
|
farChance = 5.0f; // takes about three seconds to notice (50% chance) |
|
} |
|
else // standing |
|
{ |
|
closeChance = 100.0f; |
|
farChance = 10.0f; |
|
} |
|
} |
|
|
|
// combine posture, speed, and range chances |
|
float dispositionChance = closeChance + (farChance - closeChance) * rangeModifier; |
|
|
|
// determine actual chance of noticing player |
|
float noticeChance = dispositionChance * coverRatio/100.0f; |
|
|
|
// scale by skill level |
|
noticeChance *= (0.5f + 0.5f * GetProfile()->GetSkill()); |
|
|
|
// if we are alert, our chance of noticing is much higher |
|
if (IsAlert()) |
|
{ |
|
const float alertBonus = 50.0f; |
|
noticeChance += alertBonus; |
|
} |
|
|
|
// scale by time quantum |
|
noticeChance *= deltaT / noticeQuantum; |
|
|
|
// there must always be a chance of detecting the enemy |
|
const float minChance = 0.1f; |
|
if (noticeChance < minChance) |
|
{ |
|
noticeChance = minChance; |
|
} |
|
|
|
//PrintIfWatched( "Notice chance = %3.2f\n", noticeChance ); |
|
|
|
return (RandomFloat( 0.0f, 100.0f ) < noticeChance); |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return most dangerous threat in my field of view (feeds into reaction time queue). |
|
* @todo Account for lighting levels, cover, and distance to see if we notice enemy |
|
*/ |
|
CCSPlayer *CCSBot::FindMostDangerousThreat( void ) |
|
{ |
|
VPROF_BUDGET( "CCSBot::FindMostDangerousThreat", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
if (IsBlind()) |
|
{ |
|
return NULL; |
|
} |
|
|
|
enum { MAX_THREATS = 16 }; // maximum number of simulataneously attendable threats |
|
struct CloseInfo |
|
{ |
|
CCSPlayer *enemy; |
|
float range; |
|
} |
|
threat[ MAX_THREATS ]; |
|
int threatCount = 0; |
|
|
|
int prevIndex = m_enemyQueueIndex - 1; |
|
if ( prevIndex < 0 ) |
|
prevIndex = MAX_ENEMY_QUEUE - 1; |
|
CCSPlayer *currentThreat = m_enemyQueue[ prevIndex ].player; |
|
|
|
m_bomber = NULL; |
|
m_isEnemySniperVisible = false; |
|
|
|
m_closestVisibleFriend = NULL; |
|
float closeFriendRange = 99999999999.9f; |
|
|
|
m_closestVisibleHumanFriend = NULL; |
|
float closeHumanFriendRange = 99999999999.9f; |
|
|
|
CCSPlayer *sniperThreat = NULL; |
|
float sniperThreatRange = 99999999999.9f; |
|
bool sniperThreatIsFacingMe = false; |
|
|
|
const float lookingAtMeTolerance = 0.7071f; |
|
|
|
int i; |
|
|
|
{ |
|
VPROF_BUDGET( "CCSBot::Collect Threats", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
for( i = 1; i <= gpGlobals->maxClients; ++i ) |
|
{ |
|
CBaseEntity *entity = UTIL_PlayerByIndex( i ); |
|
|
|
if (entity == NULL) |
|
continue; |
|
|
|
// is it a player? |
|
if (!entity->IsPlayer()) |
|
continue; |
|
|
|
CCSPlayer *player = static_cast<CCSPlayer *>( entity ); |
|
|
|
// ignore self |
|
if (player->entindex() == entindex()) |
|
continue; |
|
|
|
// is it alive? |
|
if (!player->IsAlive()) |
|
continue; |
|
|
|
// is it an enemy? |
|
if (player->InSameTeam( this )) |
|
{ |
|
// keep track of nearby friends - use less exact visibility check |
|
if (IsVisible( entity->WorldSpaceCenter(), false, this )) |
|
{ |
|
// update watch timestamp |
|
int idx = player->entindex(); |
|
m_watchInfo[idx].timestamp = gpGlobals->curtime; |
|
m_watchInfo[idx].isEnemy = false; |
|
|
|
// keep track of our closest friend |
|
Vector to = GetAbsOrigin() - player->GetAbsOrigin(); |
|
float rangeSq = to.LengthSqr(); |
|
if (rangeSq < closeFriendRange) |
|
{ |
|
m_closestVisibleFriend = player; |
|
closeFriendRange = rangeSq; |
|
} |
|
|
|
// keep track of our closest human friend |
|
if (!player->IsBot() && rangeSq < closeHumanFriendRange) |
|
{ |
|
m_closestVisibleHumanFriend = player; |
|
closeHumanFriendRange = rangeSq; |
|
} |
|
} |
|
|
|
continue; |
|
} |
|
|
|
// check if this enemy is fully or partially visible |
|
unsigned char visParts; |
|
if (!IsVisible( player, CHECK_FOV, &visParts )) |
|
continue; |
|
|
|
// do we notice this enemy? (always notice current enemy) |
|
if (player != currentThreat) |
|
{ |
|
if (!IsNoticable( player, visParts )) |
|
{ |
|
continue; |
|
} |
|
} |
|
|
|
// update watch timestamp |
|
int idx = player->entindex(); |
|
m_watchInfo[idx].timestamp = gpGlobals->curtime; |
|
m_watchInfo[idx].isEnemy = true; |
|
|
|
// note if we see the bomber |
|
if (player->HasC4()) |
|
{ |
|
m_bomber = player; |
|
} |
|
|
|
// keep track of all visible threats |
|
Vector d = GetAbsOrigin() - player->GetAbsOrigin(); |
|
float distSq = d.LengthSqr(); |
|
|
|
// track enemy sniper threats |
|
if (IsSniperRifle( player->GetActiveCSWeapon() )) |
|
{ |
|
m_isEnemySniperVisible = true; |
|
|
|
// keep track of the most dangerous sniper we see |
|
if (sniperThreat) |
|
{ |
|
if (IsPlayerLookingAtMe( player, lookingAtMeTolerance )) |
|
{ |
|
if (sniperThreatIsFacingMe) |
|
{ |
|
// several snipers are facing us - keep closest |
|
if (distSq < sniperThreatRange) |
|
{ |
|
sniperThreat = player; |
|
sniperThreatRange = distSq; |
|
sniperThreatIsFacingMe = true; |
|
} |
|
} |
|
else |
|
{ |
|
// even if this sniper is farther away, keep it because he's aiming at us |
|
sniperThreat = player; |
|
sniperThreatRange = distSq; |
|
sniperThreatIsFacingMe = true; |
|
} |
|
} |
|
else |
|
{ |
|
// this sniper is not looking at us, only consider it if we dont have a sniper facing us |
|
if (!sniperThreatIsFacingMe && distSq < sniperThreatRange) |
|
{ |
|
sniperThreat = player; |
|
sniperThreatRange = distSq; |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
// first sniper we see |
|
sniperThreat = player; |
|
sniperThreatRange = distSq; |
|
sniperThreatIsFacingMe = IsPlayerLookingAtMe( player, lookingAtMeTolerance ); |
|
} |
|
} |
|
|
|
|
|
{ |
|
VPROF_BUDGET( "CCSBot::Sort Threats", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
// maintain set of visible threats, sorted by increasing distance |
|
if (threatCount == 0) |
|
{ |
|
threat[0].enemy = player; |
|
threat[0].range = distSq; |
|
threatCount = 1; |
|
} |
|
else |
|
{ |
|
// find insertion point |
|
int j; |
|
for( j=0; j<threatCount; ++j ) |
|
{ |
|
if (distSq < threat[j].range) |
|
break; |
|
} |
|
|
|
// shift lower half down a notch |
|
for( int k=threatCount-1; k>=j; --k ) |
|
threat[k+1] = threat[k]; |
|
|
|
// insert threat into sorted list |
|
threat[j].enemy = player; |
|
threat[j].range = distSq; |
|
|
|
if (threatCount < MAX_THREATS) |
|
++threatCount; |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
{ |
|
VPROF_BUDGET( "CCSBot::Count nearby Friends & Enemies", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
// track the maximum enemy and friend counts we've seen recently |
|
int prevEnemies = m_nearbyEnemyCount; |
|
m_nearbyEnemyCount = 0; |
|
m_nearbyFriendCount = 0; |
|
for( i=0; i<MAX_PLAYERS; ++i ) |
|
{ |
|
if (m_watchInfo[i].timestamp <= 0.0f) |
|
continue; |
|
|
|
const float recentTime = 3.0f; |
|
if (gpGlobals->curtime - m_watchInfo[i].timestamp < recentTime) |
|
{ |
|
if (m_watchInfo[i].isEnemy) |
|
++m_nearbyEnemyCount; |
|
else |
|
++m_nearbyFriendCount; |
|
} |
|
} |
|
|
|
// note when we saw this batch of enemies |
|
if (prevEnemies == 0 && m_nearbyEnemyCount > 0) |
|
{ |
|
m_firstSawEnemyTimestamp = gpGlobals->curtime; |
|
} |
|
} |
|
|
|
|
|
{ |
|
VPROF_BUDGET( "CCSBot::Track enemy Place", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
// |
|
// Track the place where we saw most of our enemies |
|
// |
|
struct PlaceRank |
|
{ |
|
unsigned int place; |
|
int count; |
|
}; |
|
static PlaceRank placeRank[ MAX_PLACES_PER_MAP ]; |
|
int locCount = 0; |
|
|
|
PlaceRank common; |
|
common.place = 0; |
|
common.count = 0; |
|
|
|
for( i=0; i<threatCount; ++i ) |
|
{ |
|
// find the area the player/bot is standing on |
|
CNavArea *area; |
|
CCSBot *bot = dynamic_cast<CCSBot *>(threat[i].enemy); |
|
if (bot && bot->IsBot()) |
|
{ |
|
area = bot->GetLastKnownArea(); |
|
} |
|
else |
|
{ |
|
Vector enemyOrigin = GetCentroid( threat[i].enemy ); |
|
area = TheNavMesh->GetNearestNavArea( enemyOrigin ); |
|
} |
|
|
|
if (area == NULL) |
|
continue; |
|
|
|
unsigned int threatLoc = area->GetPlace(); |
|
if (!threatLoc) |
|
continue; |
|
|
|
// if place is already in set, increment count |
|
int j; |
|
for( j=0; j<locCount; ++j ) |
|
if (placeRank[j].place == threatLoc) |
|
break; |
|
|
|
if (j == locCount) |
|
{ |
|
// new place |
|
if (locCount < MAX_PLACES_PER_MAP) |
|
{ |
|
placeRank[ locCount ].place = threatLoc; |
|
placeRank[ locCount ].count = 1; |
|
|
|
if (common.count == 0) |
|
common = placeRank[locCount]; |
|
|
|
++locCount; |
|
} |
|
} |
|
else |
|
{ |
|
// others are in that place, increment |
|
++placeRank[j].count; |
|
|
|
// keep track of the most common place |
|
if (placeRank[j].count > common.count) |
|
common = placeRank[j]; |
|
} |
|
} |
|
|
|
// remember most common place |
|
m_enemyPlace = common.place; |
|
} |
|
|
|
|
|
{ |
|
VPROF_BUDGET( "CCSBot::Select Threat", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
if (threatCount == 0) |
|
return NULL; |
|
|
|
// if we can still see our current threat, keep it |
|
// unless a new one is much closer |
|
bool sawCloserThreat = false; |
|
bool sawCurrentThreat = false; |
|
int t; |
|
for( t=0; t<threatCount; ++t ) |
|
{ |
|
if ( threat[t].enemy == currentThreat ) |
|
{ |
|
sawCurrentThreat = true; |
|
} |
|
else if ( threat[t].enemy != currentThreat && |
|
IsSignificantlyCloser( threat[t].enemy, currentThreat ) ) |
|
{ |
|
sawCloserThreat = true; |
|
} |
|
} |
|
|
|
if ( sawCurrentThreat && !sawCloserThreat ) |
|
{ |
|
return currentThreat; |
|
} |
|
|
|
// if we are a sniper and we see a sniper threat, attack it unless |
|
// there are other close enemies facing me |
|
if (IsSniper() && sniperThreat) |
|
{ |
|
const float closeCombatRange = 500.0f; |
|
|
|
for( t=0; t<threatCount; ++t ) |
|
{ |
|
if (threat[t].range < closeCombatRange && IsPlayerLookingAtMe( threat[t].enemy, lookingAtMeTolerance )) |
|
{ |
|
return threat[t].enemy; |
|
} |
|
} |
|
|
|
return sniperThreat; |
|
} |
|
|
|
// otherwise, find the closest threat that is looking at me |
|
for( t=0; t<threatCount; ++t ) |
|
{ |
|
if (IsPlayerLookingAtMe( threat[t].enemy, lookingAtMeTolerance )) |
|
{ |
|
return threat[t].enemy; |
|
} |
|
} |
|
} |
|
|
|
|
|
// return closest threat |
|
return threat[0].enemy; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Update our reaction time queue |
|
*/ |
|
void CCSBot::UpdateReactionQueue( void ) |
|
{ |
|
VPROF_BUDGET( "CCSBot::UpdateReactionQueue", VPROF_BUDGETGROUP_NPCS ); |
|
|
|
// zombies dont see any threats |
|
if (cv_bot_zombie.GetBool()) |
|
return; |
|
|
|
// find biggest threat at this instant |
|
CCSPlayer *threat = FindMostDangerousThreat(); |
|
|
|
// reset timer |
|
m_attentionInterval.Start(); |
|
|
|
|
|
int now = m_enemyQueueIndex; |
|
|
|
// store a snapshot of its state at the end of the reaction time queue |
|
if (threat) |
|
{ |
|
m_enemyQueue[ now ].player = threat; |
|
m_enemyQueue[ now ].isReloading = threat->IsReloading(); |
|
m_enemyQueue[ now ].isProtectedByShield = threat->IsProtectedByShield(); |
|
} |
|
else |
|
{ |
|
m_enemyQueue[ now ].player = NULL; |
|
m_enemyQueue[ now ].isReloading = false; |
|
m_enemyQueue[ now ].isProtectedByShield = false; |
|
} |
|
|
|
// queue is round-robin |
|
++m_enemyQueueIndex; |
|
if (m_enemyQueueIndex >= MAX_ENEMY_QUEUE) |
|
m_enemyQueueIndex = 0; |
|
|
|
if (m_enemyQueueCount < MAX_ENEMY_QUEUE) |
|
++m_enemyQueueCount; |
|
|
|
// clamp reaction time to enemy queue size |
|
float reactionTime = GetProfile()->GetReactionTime() - g_BotUpdateInterval; |
|
float maxReactionTime = (MAX_ENEMY_QUEUE * g_BotUpdateInterval) - 0.01f; |
|
if (reactionTime > maxReactionTime) |
|
reactionTime = maxReactionTime; |
|
|
|
// "rewind" time back to our reaction time |
|
int reactionTimeSteps = (int)((reactionTime / g_BotUpdateInterval) + 0.5f); |
|
|
|
int i = now - reactionTimeSteps; |
|
if (i < 0) |
|
i += MAX_ENEMY_QUEUE; |
|
|
|
m_enemyQueueAttendIndex = (unsigned char)i; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return the most dangerous threat we are "conscious" of |
|
*/ |
|
CCSPlayer *CCSBot::GetRecognizedEnemy( void ) |
|
{ |
|
if (m_enemyQueueAttendIndex >= m_enemyQueueCount || IsBlind()) |
|
{ |
|
return NULL; |
|
} |
|
|
|
return m_enemyQueue[ m_enemyQueueAttendIndex ].player; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if the enemy we are "conscious" of is reloading |
|
*/ |
|
bool CCSBot::IsRecognizedEnemyReloading( void ) |
|
{ |
|
if (m_enemyQueueAttendIndex >= m_enemyQueueCount) |
|
return false; |
|
|
|
return m_enemyQueue[ m_enemyQueueAttendIndex ].isReloading; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if the enemy we are "conscious" of is hiding behind a shield |
|
*/ |
|
bool CCSBot::IsRecognizedEnemyProtectedByShield( void ) |
|
{ |
|
if (m_enemyQueueAttendIndex >= m_enemyQueueCount) |
|
return false; |
|
|
|
return m_enemyQueue[ m_enemyQueueAttendIndex ].isProtectedByShield; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return distance to closest enemy we are "conscious" of |
|
*/ |
|
float CCSBot::GetRangeToNearestRecognizedEnemy( void ) |
|
{ |
|
const CCSPlayer *enemy = GetRecognizedEnemy(); |
|
|
|
if (enemy) |
|
return (GetAbsOrigin() - enemy->GetAbsOrigin()).Length(); |
|
|
|
return 99999999.9f; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Blind the bot for the given duration |
|
*/ |
|
void CCSBot::Blind( float holdTime, float fadeTime, float startingAlpha ) |
|
{ |
|
PrintIfWatched( "Blinded: holdTime = %3.2f, fadeTime = %3.2f, alpha = %3.2f\n", holdTime, fadeTime, startingAlpha ); |
|
|
|
// if we were only blinded a little bit, shake it off |
|
const float mildBlindTime = 3.0f; |
|
if (holdTime < mildBlindTime) |
|
{ |
|
Wait( 0.75f * holdTime ); |
|
BecomeAlert(); |
|
BaseClass::Blind( holdTime, fadeTime, startingAlpha ); |
|
return; |
|
} |
|
|
|
|
|
// if blinded while in combat - then spray and pray! |
|
m_blindFire = IsAttacking(); |
|
|
|
// retreat |
|
// do this first, so spot selection happens before IsBlind() is set |
|
const float hideRange = 400.0f; |
|
TryToRetreat( hideRange ); |
|
|
|
PrintIfWatched( "I'm blind!\n" ); |
|
|
|
if (RandomFloat( 0.0f, 100.0f ) < 33.3f) |
|
{ |
|
GetChatter()->Say( "Blinded", 1.0f ); |
|
} |
|
|
|
// no longer safe |
|
AdjustSafeTime(); |
|
|
|
// decide which way to move while blind |
|
m_blindMoveDir = static_cast<NavRelativeDirType>( RandomInt( 1, NUM_RELATIVE_DIRECTIONS-1 ) ); |
|
|
|
// if we're defusing, don't give up |
|
if (IsDefusingBomb()) |
|
{ |
|
return; |
|
} |
|
|
|
// can't see to aim at enemy |
|
StopAiming(); |
|
|
|
// dont override "facing away" behavior unless we are going to spray and pray |
|
if (m_blindFire) |
|
{ |
|
ClearLookAt(); |
|
|
|
// just look straight ahead while blind |
|
Vector forward; |
|
EyeVectors( &forward ); |
|
SetLookAt( "Blind", EyePosition() + 10000.0f * forward, PRIORITY_UNINTERRUPTABLE, holdTime + 0.5f * fadeTime ); |
|
} |
|
|
|
StopWaiting(); |
|
BecomeAlert(); |
|
|
|
BaseClass::Blind( holdTime, fadeTime, startingAlpha ); |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
class CheckLookAt |
|
{ |
|
public: |
|
CheckLookAt( const CCSBot *me, bool testFOV ) |
|
{ |
|
m_me = me; |
|
m_testFOV = testFOV; |
|
} |
|
|
|
bool operator() ( CBasePlayer *player ) |
|
{ |
|
if (!m_me->IsEnemy( player )) |
|
return true; |
|
|
|
if (m_testFOV && !(const_cast< CCSBot * >(m_me)->FInViewCone( player->WorldSpaceCenter() ))) |
|
return true; |
|
|
|
if (!m_me->IsPlayerLookingAtMe( player )) |
|
return true; |
|
|
|
if (m_me->IsVisible( (CCSPlayer *)player )) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
const CCSBot *m_me; |
|
bool m_testFOV; |
|
}; |
|
|
|
/** |
|
* Return true if any enemy I have LOS to is looking directly at me |
|
* @todo Use reaction time pipeline |
|
*/ |
|
bool CCSBot::IsAnyVisibleEnemyLookingAtMe( bool testFOV ) const |
|
{ |
|
CheckLookAt checkLookAt( this, testFOV ); |
|
return (ForEachPlayer( checkLookAt ) == false) ? true : false; |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Do panic behavior |
|
*/ |
|
void CCSBot::UpdatePanicLookAround( void ) |
|
{ |
|
if (m_panicTimer.IsElapsed()) |
|
{ |
|
return; |
|
} |
|
|
|
if (IsEnemyVisible()) |
|
{ |
|
StopPanicking(); |
|
return; |
|
} |
|
|
|
if (HasLookAtTarget()) |
|
{ |
|
// wait until we finish our current look at |
|
return; |
|
} |
|
|
|
// select a spot somewhere behind us to look at as we search for our attacker |
|
const QAngle &eyeAngles = EyeAngles(); |
|
|
|
QAngle newAngles; |
|
newAngles.x = RandomFloat( -30.0f, 30.0f ); |
|
|
|
// Look directly behind at a random offset in a 90 window. |
|
float yaw = RandomFloat( 135.0f, 225.0f ); |
|
newAngles.y = eyeAngles.y + yaw; |
|
newAngles.z = 0.0f; |
|
|
|
Vector forward; |
|
AngleVectors( newAngles, &forward ); |
|
|
|
Vector spot; |
|
spot = EyePosition() + 1000.0f * forward; |
|
|
|
SetLookAt( "Panic", spot, PRIORITY_HIGH, 0.0f ); |
|
PrintIfWatched( "Panic yaw angle = %3.2f\n", newAngles.y ); |
|
}
|
|
|