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.
2069 lines
62 KiB
2069 lines
62 KiB
//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
//=============================================================================// |
|
|
|
#include "cbase.h" |
|
|
|
#include "sceneentity.h" |
|
#include "choreoevent.h" |
|
#include "choreoscene.h" |
|
#include "choreoactor.h" |
|
#include "ai_baseactor.h" |
|
#include "ai_navigator.h" |
|
#include "saverestore_utlvector.h" |
|
#include "bone_setup.h" |
|
#include "physics_npc_solver.h" |
|
#include "inforemarkable.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
ConVar flex_minplayertime( "flex_minplayertime", "5" ); |
|
ConVar flex_maxplayertime( "flex_maxplayertime", "7" ); |
|
ConVar flex_minawaytime( "flex_minawaytime", "0.5" ); |
|
ConVar flex_maxawaytime( "flex_maxawaytime", "1.0" ); |
|
ConVar ai_debug_looktargets( "ai_debug_looktargets", "0" ); |
|
ConVar ai_debug_expressions( "ai_debug_expressions", "0", FCVAR_NONE, "Show random expression decisions for NPCs." ); |
|
static ConVar scene_showfaceto( "scene_showfaceto", "0", FCVAR_ARCHIVE, "When playing back, show the directions of faceto events." ); |
|
|
|
|
|
BEGIN_DATADESC( CAI_BaseActor ) |
|
|
|
DEFINE_FIELD( m_fLatchedPositions, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_latchedEyeOrigin, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_latchedEyeDirection, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_latchedHeadDirection, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_goalHeadDirection, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_goalHeadInfluence, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_goalSpineYaw, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_goalBodyYaw, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_goalHeadCorrection, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_flBlinktime, FIELD_TIME ), |
|
DEFINE_FIELD( m_hLookTarget, FIELD_EHANDLE ), |
|
DEFINE_UTLVECTOR( m_lookQueue, FIELD_EMBEDDED ), |
|
DEFINE_UTLVECTOR( m_randomLookQueue, FIELD_EMBEDDED ), |
|
DEFINE_UTLVECTOR( m_syntheticLookQueue, FIELD_EMBEDDED ), |
|
DEFINE_FIELD( m_flNextRandomLookTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_iszExpressionScene, FIELD_STRING ), |
|
DEFINE_FIELD( m_hExpressionSceneEnt, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_flNextRandomExpressionTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_iszIdleExpression, FIELD_STRING ), |
|
DEFINE_FIELD( m_iszAlertExpression, FIELD_STRING ), |
|
DEFINE_FIELD( m_iszCombatExpression, FIELD_STRING ), |
|
DEFINE_FIELD( m_iszDeathExpression, FIELD_STRING ), |
|
//DEFINE_FIELD( m_ParameterBodyTransY, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_ParameterBodyTransX, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_ParameterBodyLift, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_ParameterBodyYaw, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_ParameterBodyPitch, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_ParameterBodyRoll, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_ParameterSpineYaw, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_ParameterSpinePitch, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_ParameterSpineRoll, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_ParameterNeckTrans, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_ParameterHeadYaw, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_ParameterHeadPitch, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_ParameterHeadRoll, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_FlexweightMoveRightLeft, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_FlexweightMoveForwardBack, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_FlexweightMoveUpDown, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_FlexweightBodyRightLeft, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_FlexweightBodyUpDown, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_FlexweightBodyTilt, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_FlexweightChestRightLeft, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_FlexweightChestUpDown, FIELD_INTEGER ), |
|
//DEFINE_FIELD( m_FlexweightChestTilt, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_FlexweightHeadForwardBack, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_FlexweightHeadRightLeft, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_FlexweightHeadUpDown, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_FlexweightHeadTilt, FIELD_INTEGER ), |
|
|
|
DEFINE_FIELD( m_ParameterGestureHeight, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_ParameterGestureWidth, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_FlexweightGestureUpDown, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_FlexweightGestureRightLeft, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_flAccumYawDelta, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_flAccumYawScale, FIELD_FLOAT ), |
|
|
|
DEFINE_ARRAY( m_flextarget, FIELD_FLOAT, 64 ), |
|
|
|
DEFINE_KEYFIELD( m_bDontUseSemaphore, FIELD_BOOLEAN, "DontUseSpeechSemaphore" ), |
|
|
|
DEFINE_KEYFIELD( m_iszExpressionOverride, FIELD_STRING, "ExpressionOverride" ), |
|
|
|
DEFINE_EMBEDDEDBYREF( m_pExpresser ), |
|
|
|
DEFINE_INPUTFUNC( FIELD_STRING, "SetExpressionOverride", InputSetExpressionOverride ), |
|
|
|
END_DATADESC() |
|
|
|
|
|
BEGIN_SIMPLE_DATADESC( CAI_InterestTarget_t ) |
|
DEFINE_FIELD( m_eType, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_hTarget, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_vecPosition, FIELD_POSITION_VECTOR ), |
|
DEFINE_FIELD( m_flStartTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_flEndTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_flRamp, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_flInterest, FIELD_FLOAT ), |
|
END_DATADESC() |
|
|
|
|
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: clear out latched state |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseActor::StudioFrameAdvance () |
|
{ |
|
// clear out head and eye latched values |
|
m_fLatchedPositions &= ~(HUMANOID_LATCHED_ALL); |
|
|
|
BaseClass::StudioFrameAdvance(); |
|
} |
|
|
|
|
|
void CAI_BaseActor::Precache() |
|
{ |
|
BaseClass::Precache(); |
|
|
|
if ( NULL_STRING != m_iszExpressionOverride ) |
|
{ |
|
PrecacheInstancedScene( STRING( m_iszExpressionOverride ) ); |
|
} |
|
|
|
if ( m_iszIdleExpression != NULL_STRING ) |
|
{ |
|
PrecacheInstancedScene( STRING(m_iszIdleExpression ) ); |
|
} |
|
|
|
if ( m_iszCombatExpression != NULL_STRING ) |
|
{ |
|
PrecacheInstancedScene( STRING(m_iszCombatExpression ) ); |
|
} |
|
|
|
if ( m_iszAlertExpression != NULL_STRING ) |
|
{ |
|
PrecacheInstancedScene( STRING(m_iszAlertExpression) ); |
|
} |
|
|
|
if ( m_iszDeathExpression != NULL_STRING ) |
|
{ |
|
PrecacheInstancedScene( STRING(m_iszDeathExpression) ); |
|
} |
|
} |
|
|
|
static char const *g_ServerSideFlexControllers[] = |
|
{ |
|
"body_rightleft", |
|
//"body_updown", |
|
//"body_tilt", |
|
"chest_rightleft", |
|
//"chest_updown", |
|
//"chest_tilt", |
|
"head_forwardback", |
|
"head_rightleft", |
|
"head_updown", |
|
"head_tilt", |
|
|
|
"gesture_updown", |
|
"gesture_rightleft" |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Static method |
|
// Input : *szName - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseActor::IsServerSideFlexController( char const *szName ) |
|
{ |
|
int c = ARRAYSIZE( g_ServerSideFlexControllers ); |
|
for ( int i = 0; i < c; ++i ) |
|
{ |
|
if ( !Q_stricmp( szName, g_ServerSideFlexControllers[ i ] ) ) |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
void CAI_BaseActor::SetModel( const char *szModelName ) |
|
{ |
|
BaseClass::SetModel( szModelName ); |
|
|
|
//Init( m_ParameterBodyTransY, "body_trans_Y" ); |
|
//Init( m_ParameterBodyTransX, "body_trans_X" ); |
|
//Init( m_ParameterBodyLift, "body_lift" ); |
|
Init( m_ParameterBodyYaw, "body_yaw" ); |
|
//Init( m_ParameterBodyPitch, "body_pitch" ); |
|
//Init( m_ParameterBodyRoll, "body_roll" ); |
|
Init( m_ParameterSpineYaw, "spine_yaw" ); |
|
//Init( m_ParameterSpinePitch, "spine_pitch" ); |
|
//Init( m_ParameterSpineRoll, "spine_roll" ); |
|
Init( m_ParameterNeckTrans, "neck_trans" ); |
|
Init( m_ParameterHeadYaw, "head_yaw" ); |
|
Init( m_ParameterHeadPitch, "head_pitch" ); |
|
Init( m_ParameterHeadRoll, "head_roll" ); |
|
|
|
//Init( m_FlexweightMoveRightLeft, "move_rightleft" ); |
|
//Init( m_FlexweightMoveForwardBack, "move_forwardback" ); |
|
//Init( m_FlexweightMoveUpDown, "move_updown" ); |
|
Init( m_FlexweightBodyRightLeft, "body_rightleft" ); |
|
//Init( m_FlexweightBodyUpDown, "body_updown" ); |
|
//Init( m_FlexweightBodyTilt, "body_tilt" ); |
|
Init( m_FlexweightChestRightLeft, "chest_rightleft" ); |
|
//Init( m_FlexweightChestUpDown, "chest_updown" ); |
|
//Init( m_FlexweightChestTilt, "chest_tilt" ); |
|
Init( m_FlexweightHeadForwardBack, "head_forwardback" ); |
|
Init( m_FlexweightHeadRightLeft, "head_rightleft" ); |
|
Init( m_FlexweightHeadUpDown, "head_updown" ); |
|
Init( m_FlexweightHeadTilt, "head_tilt" ); |
|
|
|
Init( m_ParameterGestureHeight, "gesture_height" ); |
|
Init( m_ParameterGestureWidth, "gesture_width" ); |
|
Init( m_FlexweightGestureUpDown, "gesture_updown" ); |
|
Init( m_FlexweightGestureRightLeft, "gesture_rightleft" ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseActor::StartSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ) |
|
{ |
|
Assert( info ); |
|
Assert( info->m_pScene ); |
|
Assert( info->m_pEvent ); |
|
|
|
// FIXME: this code looks duplicated |
|
switch ( info->m_pEvent->GetType() ) |
|
{ |
|
case CChoreoEvent::FACE: |
|
{ |
|
return BaseClass::StartSceneEvent( info, scene, event, actor, pTarget ); |
|
} |
|
break; |
|
|
|
case CChoreoEvent::GENERIC: |
|
{ |
|
if (stricmp( event->GetParameters(), "AI_BLINK") == 0) |
|
{ |
|
info->m_nType = SCENE_AI_BLINK; |
|
// blink eyes |
|
Blink(); |
|
// don't blink for duration, or next random blink time |
|
float flDuration = (event->GetEndTime() - scene->GetTime()); |
|
m_flBlinktime = gpGlobals->curtime + MAX( flDuration, random->RandomFloat( 1.5, 4.5 ) ); |
|
} |
|
else if (stricmp( event->GetParameters(), "AI_HOLSTER") == 0) |
|
{ |
|
// FIXME: temp code for test |
|
info->m_nType = SCENE_AI_HOLSTER; |
|
info->m_iLayer = HolsterWeapon(); |
|
return true; |
|
} |
|
else if (stricmp( event->GetParameters(), "AI_UNHOLSTER") == 0) |
|
{ |
|
// FIXME: temp code for test |
|
info->m_nType = SCENE_AI_UNHOLSTER; |
|
info->m_iLayer = UnholsterWeapon(); |
|
return true; |
|
} |
|
else if (stricmp( event->GetParameters(), "AI_AIM") == 0) |
|
{ |
|
info->m_nType = SCENE_AI_AIM; |
|
info->m_hTarget = pTarget; |
|
} |
|
else if (stricmp( event->GetParameters(), "AI_RANDOMLOOK") == 0) |
|
{ |
|
info->m_nType = SCENE_AI_RANDOMLOOK; |
|
info->m_flNext = 0.0; |
|
} |
|
else if (stricmp( event->GetParameters(), "AI_RANDOMFACEFLEX") == 0) |
|
{ |
|
info->m_nType = SCENE_AI_RANDOMFACEFLEX; |
|
info->m_flNext = 0.0; |
|
info->InitWeight( this ); |
|
} |
|
else if (stricmp( event->GetParameters(), "AI_RANDOMHEADFLEX") == 0) |
|
{ |
|
info->m_nType = SCENE_AI_RANDOMHEADFLEX; |
|
info->m_flNext = 0.0; |
|
} |
|
else if (stricmp( event->GetParameters(), "AI_IGNORECOLLISION") == 0) |
|
{ |
|
CBaseEntity *pTarget = FindNamedEntity( event->GetParameters2( ) ); |
|
|
|
if (pTarget) |
|
{ |
|
info->m_nType = SCENE_AI_IGNORECOLLISION; |
|
info->m_hTarget = pTarget; |
|
float remaining = event->GetEndTime() - scene->GetTime(); |
|
NPCPhysics_CreateSolver( this, pTarget, true, remaining ); |
|
info->m_flNext = gpGlobals->curtime + remaining; |
|
return true; |
|
} |
|
else |
|
{ |
|
Warning( "CSceneEntity %s unable to find actor named \"%s\"\n", scene->GetFilename(), event->GetParameters2() ); |
|
return false; |
|
} |
|
} |
|
else if (stricmp( event->GetParameters(), "AI_DISABLEAI") == 0) |
|
{ |
|
info->m_nType = SCENE_AI_DISABLEAI; |
|
} |
|
else |
|
{ |
|
return BaseClass::StartSceneEvent( info, scene, event, actor, pTarget ); |
|
} |
|
return true; |
|
} |
|
break; |
|
|
|
default: |
|
return BaseClass::StartSceneEvent( info, scene, event, actor, pTarget ); |
|
} |
|
} |
|
|
|
|
|
bool CAI_BaseActor::ProcessSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
Assert( info ); |
|
Assert( info->m_pScene ); |
|
Assert( info->m_pEvent ); |
|
|
|
// FIXME: this code looks duplicated |
|
switch ( info->m_pEvent->GetType() ) |
|
{ |
|
case CChoreoEvent::FACE: |
|
{ |
|
// make sure target exists |
|
if (info->m_hTarget == NULL) |
|
return false; |
|
|
|
bool bInScene = false; |
|
|
|
// lockbodyfacing is designed to run on top of both normal AI and on top of |
|
// scripted_sequences. By allowing torso turns during post-idles, pre-idles, |
|
// act-busy's, scripted_sequences, normal AI movements, etc., it increases |
|
// the functionality of those AI features without breaking their assuptions |
|
// that the entity won't be made to "turn" by something outside of those |
|
// AI's control. |
|
// lockbody facing is also usefull when npcs are moving and you want them to turn |
|
// towards something but still walk in the direction of travel. |
|
if (!event->IsLockBodyFacing()) |
|
bInScene = EnterSceneSequence( scene, event, true ); |
|
|
|
// make sure we're still able to play this command |
|
if (!info->m_bStarted) |
|
{ |
|
info->m_flInitialYaw = GetLocalAngles().y; |
|
info->m_flTargetYaw = info->m_flInitialYaw; |
|
info->m_flFacingYaw = info->m_flInitialYaw; |
|
if (IsMoving()) |
|
{ |
|
info->m_flWeight = 1.0; |
|
} |
|
else |
|
{ |
|
info->m_flWeight = 0.0; |
|
} |
|
} |
|
|
|
// lock in place if aiming at self |
|
if (info->m_hTarget == this) |
|
{ |
|
return true; |
|
} |
|
|
|
if (!bInScene || info->m_bIsMoving != IsMoving()) |
|
{ |
|
info->m_flInitialYaw = GetLocalAngles().y; |
|
} |
|
info->m_bIsMoving = IsMoving(); |
|
|
|
// Msg("%f : %f - %f\n", scene->GetTime(), event->GetStartTime(), event->GetEndTime() ); |
|
float flTime = clamp( scene->GetTime(), event->GetStartTime(), event->GetEndTime() - 0.1 ); |
|
float intensity = event->GetIntensity( flTime ); |
|
|
|
// clamp in-ramp to 0.5 seconds |
|
float flDuration = scene->GetTime() - event->GetStartTime(); |
|
float flMaxIntensity = flDuration < 0.5f ? SimpleSpline( flDuration / 0.5f ) : 1.0f; |
|
intensity = clamp( intensity, 0.0f, flMaxIntensity ); |
|
|
|
if (bInScene && info->m_bIsMoving) |
|
{ |
|
info->m_flInitialYaw = GetLocalAngles().y; |
|
} |
|
|
|
if (!event->IsLockBodyFacing()) |
|
{ |
|
if (!info->m_bIsMoving && bInScene) |
|
{ |
|
AccumulateIdealYaw( info->m_flFacingYaw, intensity ); |
|
} |
|
} |
|
|
|
float diff; |
|
float dir; |
|
float flSpineYaw; |
|
float flBodyYaw; |
|
|
|
// move upper body to account for missing body yaw |
|
diff = UTIL_AngleDiff( info->m_flTargetYaw, GetLocalAngles().y ); |
|
if (diff < 0) |
|
{ |
|
diff = -diff; |
|
dir = -1; |
|
} |
|
else |
|
{ |
|
dir = 1; |
|
} |
|
flSpineYaw = MIN( diff, 30 ); |
|
flBodyYaw = MIN( diff - flSpineYaw, 30 ); |
|
m_goalSpineYaw = m_goalSpineYaw * (1.0 - intensity) + intensity * flSpineYaw * dir; |
|
m_goalBodyYaw = m_goalBodyYaw * (1.0 - intensity) + intensity * flBodyYaw * dir; |
|
|
|
/* |
|
NDebugOverlay::YawArrow( GetAbsOrigin(), GetLocalAngles().y, 64, 16, 255, 255, 255, 0, true, 0.1 ); |
|
NDebugOverlay::YawArrow( GetAbsOrigin() + Vector( 0, 0, 8 ), GetLocalAngles().y + m_goalBodyYaw, 64, 16, 255, 128, 128, 0, true, 0.1 ); |
|
NDebugOverlay::YawArrow( GetAbsOrigin() + Vector( 0, 0, 16 ), GetLocalAngles().y + m_goalSpineYaw, 64, 16, 128, 255, 128, 0, true, 0.1 ); |
|
NDebugOverlay::YawArrow( GetAbsOrigin() + Vector( 0, 0, 24 ), info->m_flTargetYaw, 64, 16, 128, 128, 255, 0, true, 0.1 ); |
|
*/ |
|
|
|
CAI_BaseNPC *pGoalNpc = info->m_hTarget->MyNPCPointer(); |
|
|
|
float goalYaw = GetLocalAngles().y; |
|
|
|
if ( pGoalNpc ) |
|
{ |
|
goalYaw = CalcIdealYaw( pGoalNpc->FacingPosition() ); |
|
} |
|
else |
|
{ |
|
goalYaw = CalcIdealYaw( info->m_hTarget->EyePosition() ); |
|
} |
|
|
|
if (developer.GetInt() > 0 && scene_showfaceto.GetBool()) |
|
{ |
|
NDebugOverlay::YawArrow( GetAbsOrigin() + Vector( 0, 0, 1 ), goalYaw, 8 + 32 * intensity, 8, 255, 255, 255, 0, true, 0.12 ); |
|
} |
|
|
|
diff = UTIL_AngleDiff( goalYaw, info->m_flInitialYaw ) * intensity; |
|
dir = 1.0; |
|
|
|
// debounce delta a bit |
|
info->m_flTargetYaw = UTIL_AngleMod( info->m_flInitialYaw + diff ); |
|
|
|
if (diff < 0) |
|
{ |
|
diff = -diff; |
|
dir = -1; |
|
} |
|
|
|
// calc how much to use the spine for turning |
|
float spineintensity = (1.0 - MAX( 0.0, (intensity - 0.5) / 0.5 )); |
|
// force spine to full if not in scene or locked |
|
if (!bInScene || event->IsLockBodyFacing() ) |
|
{ |
|
spineintensity = 1.0; |
|
} |
|
|
|
flSpineYaw = MIN( diff * spineintensity, 30 ); |
|
flBodyYaw = MIN( diff * spineintensity - flSpineYaw, 30 ); |
|
info->m_flFacingYaw = info->m_flInitialYaw + (diff - flBodyYaw - flSpineYaw) * dir; |
|
|
|
if (!event->IsLockBodyFacing()) |
|
{ |
|
AddFacingTarget( info->m_hTarget, intensity, 0.2 ); // facing targets are lagged by one frame |
|
} |
|
return true; |
|
} |
|
case CChoreoEvent::GENERIC: |
|
{ |
|
switch(info->m_nType) |
|
{ |
|
case SCENE_AI_BLINK: |
|
{ |
|
// keep eyes not blinking for duration |
|
float flDuration = (event->GetEndTime() - scene->GetTime()); |
|
m_flBlinktime = MAX( m_flBlinktime, gpGlobals->curtime + flDuration ); |
|
} |
|
return true; |
|
case SCENE_AI_HOLSTER: |
|
{ |
|
} |
|
return true; |
|
case SCENE_AI_UNHOLSTER: |
|
{ |
|
} |
|
return true; |
|
case SCENE_AI_AIM: |
|
{ |
|
if ( info->m_hTarget ) |
|
{ |
|
Vector vecAimTargetLoc = info->m_hTarget->EyePosition(); |
|
Vector vecAimDir = vecAimTargetLoc - EyePosition(); |
|
|
|
VectorNormalize( vecAimDir ); |
|
SetAim( vecAimDir); |
|
} |
|
} |
|
return true; |
|
case SCENE_AI_RANDOMLOOK: |
|
{ |
|
if (info->m_flNext < gpGlobals->curtime) |
|
{ |
|
info->m_flNext = gpGlobals->curtime + PickLookTarget( m_syntheticLookQueue ) - 0.4; |
|
if (m_syntheticLookQueue.Count() > 0) |
|
{ |
|
float flDuration = (event->GetEndTime() - scene->GetTime()); |
|
int i = m_syntheticLookQueue.Count() - 1; |
|
m_syntheticLookQueue[i].m_flEndTime = MIN( m_syntheticLookQueue[i].m_flEndTime, gpGlobals->curtime + flDuration ); |
|
m_syntheticLookQueue[i].m_flInterest = 0.1; |
|
} |
|
} |
|
} |
|
return true; |
|
case SCENE_AI_RANDOMFACEFLEX: |
|
return RandomFaceFlex( info, scene, event ); |
|
case SCENE_AI_RANDOMHEADFLEX: |
|
return true; |
|
case SCENE_AI_IGNORECOLLISION: |
|
if (info->m_hTarget && info->m_flNext < gpGlobals->curtime) |
|
{ |
|
float remaining = event->GetEndTime() - scene->GetTime(); |
|
NPCPhysics_CreateSolver( this, info->m_hTarget, true, remaining ); |
|
info->m_flNext = gpGlobals->curtime + remaining; |
|
} |
|
|
|
// FIXME: needs to handle scene pause |
|
return true; |
|
case SCENE_AI_DISABLEAI: |
|
if (!(GetState() == NPC_STATE_SCRIPT || IsCurSchedule( SCHED_SCENE_GENERIC )) ) |
|
{ |
|
EnterSceneSequence( scene, event ); |
|
} |
|
return true; |
|
default: |
|
return false; |
|
} |
|
} |
|
break; |
|
default: |
|
return BaseClass::ProcessSceneEvent( info, scene, event ); |
|
} |
|
} |
|
|
|
|
|
bool CAI_BaseActor::RandomFaceFlex( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
if (info->m_flNext < gpGlobals->curtime) |
|
{ |
|
const flexsettinghdr_t *pSettinghdr = ( const flexsettinghdr_t * )FindSceneFile( event->GetParameters2() ); |
|
if (pSettinghdr == NULL) |
|
{ |
|
pSettinghdr = ( const flexsettinghdr_t * )FindSceneFile( "random" ); |
|
} |
|
if ( pSettinghdr ) |
|
{ |
|
info->m_flNext = gpGlobals->curtime + random->RandomFloat( 0.3, 0.5 ) * (30.0 / pSettinghdr->numflexsettings); |
|
|
|
flexsetting_t const *pSetting = NULL; |
|
pSetting = pSettinghdr->pSetting( random->RandomInt( 0, pSettinghdr->numflexsettings - 1 ) ); |
|
|
|
flexweight_t *pWeights = NULL; |
|
int truecount = pSetting->psetting( (byte *)pSettinghdr, 0, &pWeights ); |
|
if ( !pWeights ) |
|
return false; |
|
|
|
int i; |
|
for (i = 0; i < truecount; i++, pWeights++) |
|
{ |
|
// Translate to local flex controller |
|
// this is translating from the settings's local index to the models local index |
|
int index = FlexControllerLocalToGlobal( pSettinghdr, pWeights->key ); |
|
|
|
// FIXME: this is supposed to blend based on pWeight->influence, but the order is wrong... |
|
// float value = GetFlexWeight( index ) * (1 - scale * pWeights->influence) + scale * pWeights->weight; |
|
|
|
// Add scaled weighting in to total |
|
m_flextarget[ index ] = pWeights->weight; |
|
} |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
} |
|
|
|
// adjust intensity if this is a background scene and there's other flex animations playing |
|
float intensity = info->UpdateWeight( this ) * event->GetIntensity( scene->GetTime() ); |
|
|
|
// slide it up. |
|
for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) |
|
{ |
|
float weight = GetFlexWeight( i ); |
|
|
|
if (weight != m_flextarget[i]) |
|
{ |
|
float delta = (m_flextarget[i] - weight) / random->RandomFloat( 2.0, 4.0 ); |
|
weight = weight + delta * intensity; |
|
} |
|
weight = clamp( weight, 0.0f, 1.0f ); |
|
SetFlexWeight( i, weight ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
bool CAI_BaseActor::ClearSceneEvent( CSceneEventInfo *info, bool fastKill, bool canceled ) |
|
{ |
|
Assert( info ); |
|
Assert( info->m_pScene ); |
|
Assert( info->m_pEvent ); |
|
|
|
// FIXME: this code looks duplicated |
|
switch ( info->m_pEvent->GetType() ) |
|
{ |
|
case CChoreoEvent::FACE: |
|
{ |
|
return BaseClass::ClearSceneEvent( info, fastKill, canceled ); |
|
} |
|
break; |
|
default: |
|
return BaseClass::ClearSceneEvent( info, fastKill, canceled ); |
|
} |
|
} |
|
|
|
|
|
|
|
bool CAI_BaseActor::CheckSceneEventCompletion( CSceneEventInfo *info, float currenttime, CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
Assert( info ); |
|
Assert( info->m_pScene ); |
|
Assert( info->m_pEvent ); |
|
|
|
switch ( event->GetType() ) |
|
{ |
|
case CChoreoEvent::GENERIC: |
|
{ |
|
switch( info->m_nType) |
|
{ |
|
case SCENE_AI_HOLSTER: |
|
case SCENE_AI_UNHOLSTER: |
|
{ |
|
if (info->m_iLayer == -1) |
|
{ |
|
return true; |
|
} |
|
float preload = event->GetEndTime() - currenttime; |
|
if (preload < 0) |
|
{ |
|
return true; |
|
} |
|
float t = (1.0 - GetLayerCycle( info->m_iLayer )) * SequenceDuration( GetLayerSequence( info->m_iLayer ) ); |
|
|
|
return (t <= preload); |
|
} |
|
} |
|
} |
|
} |
|
|
|
return BaseClass::CheckSceneEventCompletion( info, currenttime, scene, event ); |
|
} |
|
|
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: clear out latched state |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseActor::SetViewtarget( const Vector &viewtarget ) |
|
{ |
|
// clear out eye latch |
|
m_fLatchedPositions &= ~HUMANOID_LATCHED_EYE; |
|
|
|
BaseClass::SetViewtarget( viewtarget ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Clear out head/look targets |
|
//----------------------------------------------------------------------------- |
|
|
|
|
|
void CAI_BaseActor::ClearHeadAdjustment() |
|
{ |
|
//DevMsg( "Teleport %s : %.0f %.0f %.0f : %.0f %.0f %.0f\n", GetEntityNameAsCStr(), Get( m_ParameterHeadYaw ), Get( m_ParameterHeadPitch ), Get( m_ParameterHeadRoll ), m_goalHeadCorrection.x, m_goalHeadCorrection.y, m_goalHeadCorrection.z ); |
|
m_lookQueue.RemoveAll(); |
|
m_syntheticLookQueue.RemoveAll(); |
|
m_randomLookQueue.RemoveAll(); |
|
|
|
Set( m_ParameterHeadYaw, 0.0f ); |
|
Set( m_ParameterHeadPitch, 0.0f ); |
|
Set( m_ParameterHeadRoll, 0.0f ); |
|
|
|
m_goalHeadDirection.Init(); |
|
m_goalHeadInfluence = 0.0f; |
|
|
|
m_goalSpineYaw = 0.0f; |
|
m_goalBodyYaw = 0.0f; |
|
m_goalHeadCorrection.Init(); |
|
} |
|
|
|
|
|
void CAI_BaseActor::Teleport( const Vector *newPosition, const QAngle *newAngles, const Vector *newVelocity ) |
|
{ |
|
ClearHeadAdjustment(); |
|
|
|
BaseClass::Teleport( newPosition, newAngles, newVelocity ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Clear out eye/head latched positions |
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseActor::InvalidateBoneCache() |
|
{ |
|
m_fLatchedPositions &= ~(HUMANOID_LATCHED_ALL); |
|
|
|
BaseClass::InvalidateBoneCache(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns true position of the eyeballs |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseActor::UpdateLatchedValues( ) |
|
{ |
|
if (!(m_fLatchedPositions & HUMANOID_LATCHED_HEAD)) |
|
{ |
|
// set head latch |
|
m_fLatchedPositions |= HUMANOID_LATCHED_HEAD; |
|
|
|
if ( CanSkipAnimation() || !GetAttachment( "eyes", m_latchedEyeOrigin, &m_latchedHeadDirection )) |
|
{ |
|
m_latchedEyeOrigin = BaseClass::EyePosition( ); |
|
AngleVectors( GetLocalAngles(), &m_latchedHeadDirection ); |
|
} |
|
// clear out eye latch |
|
m_fLatchedPositions &= ~(HUMANOID_LATCHED_EYE); |
|
// DevMsg( "eyeball %4f %4f %4f : %3f %3f %3f\n", origin.x, origin.y, origin.z, angles.x, angles.y, angles.z ); |
|
} |
|
|
|
if (!(m_fLatchedPositions & HUMANOID_LATCHED_EYE)) |
|
{ |
|
m_fLatchedPositions |= HUMANOID_LATCHED_EYE; |
|
|
|
if ( CapabilitiesGet() & bits_CAP_ANIMATEDFACE ) |
|
{ |
|
m_latchedEyeDirection = GetViewtarget() - m_latchedEyeOrigin; |
|
VectorNormalize( m_latchedEyeDirection ); |
|
} |
|
else |
|
{ |
|
m_latchedEyeDirection = m_latchedHeadDirection; |
|
} |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns true position of the eyeballs |
|
//----------------------------------------------------------------------------- |
|
Vector CAI_BaseActor::EyePosition( ) |
|
{ |
|
UpdateLatchedValues(); |
|
|
|
return m_latchedEyeOrigin; |
|
} |
|
|
|
|
|
#define MIN_LOOK_TARGET_DIST 1.0f |
|
#define MAX_FULL_LOOK_TARGET_DIST 10.0f |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns true if target is in legal range of eye movement for the current head position |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseActor::ValidEyeTarget(const Vector &lookTargetPos) |
|
{ |
|
Vector vHeadDir = HeadDirection3D( ); |
|
Vector lookTargetDir = lookTargetPos - EyePosition(); |
|
float flDist = VectorNormalize(lookTargetDir); |
|
|
|
if (flDist < MIN_LOOK_TARGET_DIST) |
|
{ |
|
return false; |
|
} |
|
|
|
// Only look if it doesn't crank my eyeballs too far |
|
float dotPr = DotProduct(lookTargetDir, vHeadDir); |
|
// DevMsg( "ValidEyeTarget( %4f %4f %4f ) %3f\n", lookTargetPos.x, lookTargetPos.y, lookTargetPos.z, dotPr ); |
|
|
|
if (dotPr > 0.259) // +- 75 degrees |
|
// if (dotPr > 0.86) // +- 30 degrees |
|
{ |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns true if target is in legal range of possible head movements |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseActor::ValidHeadTarget(const Vector &lookTargetPos) |
|
{ |
|
Vector vFacing = BodyDirection3D(); |
|
Vector lookTargetDir = lookTargetPos - EyePosition(); |
|
float flDist = VectorNormalize(lookTargetDir); |
|
|
|
if (flDist < MIN_LOOK_TARGET_DIST) |
|
{ |
|
return false; |
|
} |
|
|
|
// Only look if it doesn't crank my head too far |
|
float dotPr = DotProduct(lookTargetDir, vFacing); |
|
if (dotPr > 0 && fabs( lookTargetDir.z ) < 0.7) // +- 90 degrees side to side, +- 45 up/down |
|
{ |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns how much to try to look at the target |
|
//----------------------------------------------------------------------------- |
|
float CAI_BaseActor::HeadTargetValidity(const Vector &lookTargetPos) |
|
{ |
|
Vector vFacing = BodyDirection3D(); |
|
|
|
int iForward = LookupAttachment( "forward" ); |
|
if (iForward) |
|
{ |
|
Vector tmp1; |
|
GetAttachment( iForward, tmp1, &vFacing, NULL, NULL ); |
|
} |
|
|
|
Vector lookTargetDir = lookTargetPos - EyePosition(); |
|
float flDist = lookTargetDir.Length2D(); |
|
VectorNormalize(lookTargetDir); |
|
|
|
if (flDist <= MIN_LOOK_TARGET_DIST) |
|
{ |
|
return 0; |
|
} |
|
|
|
// Only look if it doesn't crank my head too far |
|
float dotPr = DotProduct(lookTargetDir, vFacing); |
|
// only look if target is within +-135 degrees |
|
// scale 1..-0.707 == 1..1, -.707..-1 == 1..0 |
|
// X * b + b = 1 == 1 / (X + 1) = b, 3.4142 |
|
float flInterest = clamp( 3.4142 + 3.4142 * dotPr, 0, 1 ); |
|
|
|
// stop looking when point too close |
|
if (flDist < MAX_FULL_LOOK_TARGET_DIST) |
|
{ |
|
flInterest = flInterest * (flDist - MIN_LOOK_TARGET_DIST ) / (MAX_FULL_LOOK_TARGET_DIST - MIN_LOOK_TARGET_DIST); |
|
} |
|
|
|
return flInterest; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Integrate head turn over time |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseActor::SetHeadDirection( const Vector &vTargetPos, float flInterval) |
|
{ |
|
Assert(0); // Actors shouldn't be calling this, it doesn't do anything |
|
} |
|
|
|
float CAI_BaseActor::ClampWithBias( PoseParameter_t index, float value, float base ) |
|
{ |
|
return EdgeLimitPoseParameter( (int)index, value, base ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Accumulate all the wanted yaw changes |
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseActor::AccumulateIdealYaw( float flYaw, float flIntensity ) |
|
{ |
|
float diff = AngleDiff( flYaw, GetLocalAngles().y ); |
|
m_flAccumYawDelta += diff * flIntensity; |
|
m_flAccumYawScale += flIntensity; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: do any pending yaw movements |
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseActor::SetAccumulatedYawAndUpdate( void ) |
|
{ |
|
if (m_flAccumYawScale > 0.0) |
|
{ |
|
float diff = m_flAccumYawDelta / m_flAccumYawScale; |
|
float facing = GetLocalAngles().y + diff; |
|
|
|
m_flAccumYawDelta = 0.0; |
|
m_flAccumYawScale = 0.0; |
|
|
|
if (IsCurSchedule( SCHED_SCENE_GENERIC )) |
|
{ |
|
if (!IsMoving()) |
|
{ |
|
GetMotor()->SetIdealYawAndUpdate( facing ); |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: match actors "forward" attachment to point in direction of vHeadTarget |
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseActor::UpdateBodyControl( ) |
|
{ |
|
// FIXME: only during idle, or in response to accel/decel |
|
//Set( m_ParameterBodyTransY, Get( m_FlexweightMoveRightLeft ) ); |
|
//Set( m_ParameterBodyTransX, Get( m_FlexweightMoveForwardBack ) ); |
|
//Set( m_ParameterBodyLift, Get( m_FlexweightMoveUpDown ) ); |
|
Set( m_ParameterBodyYaw, Get( m_FlexweightBodyRightLeft ) + m_goalBodyYaw ); |
|
//Set( m_ParameterBodyPitch, Get( m_FlexweightBodyUpDown ) ); |
|
//Set( m_ParameterBodyRoll, Get( m_FlexweightBodyTilt ) ); |
|
Set( m_ParameterSpineYaw, Get( m_FlexweightChestRightLeft ) + m_goalSpineYaw ); |
|
//Set( m_ParameterSpinePitch, Get( m_FlexweightChestUpDown ) ); |
|
//Set( m_ParameterSpineRoll, Get( m_FlexweightChestTilt ) ); |
|
Set( m_ParameterNeckTrans, Get( m_FlexweightHeadForwardBack ) ); |
|
} |
|
|
|
|
|
static ConVar scene_clamplookat( "scene_clamplookat", "1", FCVAR_NONE, "Clamp head turns to a MAX of 20 degrees per think." ); |
|
|
|
|
|
void CAI_BaseActor::UpdateHeadControl( const Vector &vHeadTarget, float flHeadInfluence ) |
|
{ |
|
float flTarget; |
|
float flLimit; |
|
|
|
if (!(CapabilitiesGet() & bits_CAP_TURN_HEAD)) |
|
{ |
|
return; |
|
} |
|
|
|
// calc current animation head bias, movement needs to clamp accumulated with this |
|
QAngle angBias; |
|
QAngle vTargetAngles; |
|
|
|
int iEyes = LookupAttachment( "eyes" ); |
|
int iChest = LookupAttachment( "chest" ); |
|
int iForward = LookupAttachment( "forward" ); |
|
|
|
matrix3x4_t eyesToWorld; |
|
matrix3x4_t forwardToWorld, worldToForward; |
|
|
|
if (iEyes <= 0 || iForward <= 0) |
|
{ |
|
// Head control on model without "eyes" or "forward" attachment |
|
// Most likely this is a cheaple or a generic_actor set to a model that doesn't support head/eye turning. |
|
// DevWarning( "%s using model \"%s\" that doesn't support head turning\n", GetClassname(), STRING( GetModelName() ) ); |
|
CapabilitiesRemove( bits_CAP_TURN_HEAD ); |
|
return; |
|
} |
|
|
|
GetAttachment( iEyes, eyesToWorld ); |
|
|
|
GetAttachment( iForward, forwardToWorld ); |
|
MatrixInvert( forwardToWorld, worldToForward ); |
|
|
|
// Lookup chest attachment to do compounded range limit checks |
|
if (iChest > 0) |
|
{ |
|
matrix3x4_t chestToWorld, worldToChest; |
|
GetAttachment( iChest, chestToWorld ); |
|
MatrixInvert( chestToWorld, worldToChest ); |
|
matrix3x4_t tmpM; |
|
ConcatTransforms( worldToChest, eyesToWorld, tmpM ); |
|
MatrixAngles( tmpM, angBias ); |
|
|
|
angBias.y -= Get( m_ParameterHeadYaw ); |
|
angBias.x -= Get( m_ParameterHeadPitch ); |
|
angBias.z -= Get( m_ParameterHeadRoll ); |
|
|
|
/* |
|
if ( (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) ) |
|
{ |
|
// Msg("bias %f %f %f\n", angBias.x, angBias.y, angBias.z ); |
|
|
|
Vector tmp1, tmp2; |
|
|
|
VectorTransform( Vector( 0, 0, 0), chestToWorld, tmp1 ); |
|
VectorTransform( Vector( 100, 0, 0), chestToWorld, tmp2 ); |
|
NDebugOverlay::Line( tmp1, tmp2, 0,0,255, false, 0.12 ); |
|
|
|
VectorTransform( Vector( 0, 0, 0), eyesToWorld, tmp1 ); |
|
VectorTransform( Vector( 100, 0, 0), eyesToWorld, tmp2 ); |
|
NDebugOverlay::Line( tmp1, tmp2, 0,0,255, false, 0.12 ); |
|
|
|
// NDebugOverlay::Line( EyePosition(), pEntity->EyePosition(), 0,0,255, false, 0.5); |
|
} |
|
*/ |
|
} |
|
else |
|
{ |
|
angBias.Init( 0, 0, 0 ); |
|
} |
|
|
|
matrix3x4a_t targetXform; |
|
targetXform = forwardToWorld; |
|
Vector vTargetDir = vHeadTarget - EyePosition(); |
|
|
|
if (scene_clamplookat.GetBool()) |
|
{ |
|
// scale down pitch when the target is behind the head |
|
Vector vTargetLocal; |
|
VectorNormalize( vTargetDir ); |
|
VectorIRotate( vTargetDir, forwardToWorld, vTargetLocal ); |
|
vTargetLocal.z *= clamp( vTargetLocal.x, 0.1, 1.0 ); |
|
VectorNormalize( vTargetLocal ); |
|
VectorRotate( vTargetLocal, forwardToWorld, vTargetDir ); |
|
|
|
// clamp local influence when target is behind the head |
|
flHeadInfluence = flHeadInfluence * clamp( vTargetLocal.x * 2.0 + 2.0, 0.0, 1.0 ); |
|
} |
|
|
|
Studio_AlignIKMatrix( targetXform, vTargetDir ); |
|
|
|
matrix3x4_t headXform; |
|
ConcatTransforms( worldToForward, targetXform, headXform ); |
|
MatrixAngles( headXform, vTargetAngles ); |
|
|
|
// partially debounce head goal |
|
float s0 = 1.0 - flHeadInfluence + GetHeadDebounce() * flHeadInfluence; |
|
float s1 = (1.0 - s0); |
|
// limit velocity of head turns |
|
m_goalHeadCorrection.x = UTIL_Approach( m_goalHeadCorrection.x * s0 + vTargetAngles.x * s1, m_goalHeadCorrection.x, 10.0 ); |
|
m_goalHeadCorrection.y = UTIL_Approach( m_goalHeadCorrection.y * s0 + vTargetAngles.y * s1, m_goalHeadCorrection.y, 30.0 ); |
|
m_goalHeadCorrection.z = UTIL_Approach( m_goalHeadCorrection.z * s0 + vTargetAngles.z * s1, m_goalHeadCorrection.z, 10.0 ); |
|
|
|
/* |
|
if ( (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) ) |
|
{ |
|
// Msg( "yaw %.1f (%f) pitch %.1f (%.1f)\n", m_goalHeadCorrection.y, vTargetAngles.y, vTargetAngles.x, m_goalHeadCorrection.x ); |
|
// Msg( "yaw %.2f (goal %.2f) (influence %.2f) (flex %.2f)\n", flLimit, m_goalHeadCorrection.y, flHeadInfluence, Get( m_FlexweightHeadRightLeft ) ); |
|
} |
|
*/ |
|
|
|
flTarget = m_goalHeadCorrection.y + Get( m_FlexweightHeadRightLeft ); |
|
flLimit = ClampWithBias( m_ParameterHeadYaw, flTarget, angBias.y ); |
|
/* |
|
if ( (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) ) |
|
{ |
|
Msg( "yaw %5.1f : (%5.1f : %5.1f) %5.1f (%5.1f)\n", flLimit, m_goalHeadCorrection.y, Get( m_FlexweightHeadRightLeft ), angBias.y, Get( m_ParameterHeadYaw ) ); |
|
} |
|
*/ |
|
Set( m_ParameterHeadYaw, flLimit ); |
|
|
|
flTarget = m_goalHeadCorrection.x + Get( m_FlexweightHeadUpDown ); |
|
flLimit = ClampWithBias( m_ParameterHeadPitch, flTarget, angBias.x ); |
|
/* |
|
if ( (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) ) |
|
{ |
|
Msg( "pitch %5.1f : (%5.1f : %5.1f) %5.1f (%5.1f)\n", flLimit, m_goalHeadCorrection.x, Get( m_FlexweightHeadUpDown ), angBias.x, Get( m_ParameterHeadPitch ) ); |
|
} |
|
*/ |
|
Set( m_ParameterHeadPitch, flLimit ); |
|
|
|
flTarget = m_goalHeadCorrection.z + Get( m_FlexweightHeadTilt ); |
|
flLimit = ClampWithBias( m_ParameterHeadRoll, flTarget, angBias.z ); |
|
/* |
|
if ( (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) ) |
|
{ |
|
Msg( "roll %5.1f : (%5.1f : %5.1f) %5.1f (%5.1f)\n", flLimit, m_goalHeadCorrection.z, Get( m_FlexweightHeadTilt ), angBias.z, Get( m_ParameterHeadRoll ) ); |
|
} |
|
*/ |
|
Set( m_ParameterHeadRoll, flLimit ); |
|
} |
|
|
|
|
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : Calculate the NPC's eye direction in 2D world space |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
Vector CAI_BaseActor::EyeDirection2D( void ) |
|
{ |
|
Vector vEyeDirection = EyeDirection3D( ); |
|
vEyeDirection.z = 0; |
|
|
|
vEyeDirection.AsVector2D().NormalizeInPlace(); |
|
|
|
return vEyeDirection; |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : Calculate the NPC's eye direction in 2D world space |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
Vector CAI_BaseActor::EyeDirection3D( void ) |
|
{ |
|
UpdateLatchedValues( ); |
|
|
|
return m_latchedEyeDirection; |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : Calculate the NPC's head direction in 2D world space |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
Vector CAI_BaseActor::HeadDirection2D( void ) |
|
{ |
|
Vector vHeadDirection = HeadDirection3D(); |
|
vHeadDirection.z = 0; |
|
vHeadDirection.AsVector2D().NormalizeInPlace(); |
|
return vHeadDirection; |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose : Calculate the NPC's head direction in 3D world space |
|
// Input : |
|
// Output : |
|
//------------------------------------------------------------------------------ |
|
Vector CAI_BaseActor::HeadDirection3D( ) |
|
{ |
|
UpdateLatchedValues( ); |
|
|
|
return m_latchedHeadDirection; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CAI_BaseActor::HasActiveLookTargets( void ) |
|
{ |
|
return m_lookQueue.Count() != 0; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Clear any active look targets for the specified entity |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseActor::ClearLookTarget( CBaseEntity *pTarget ) |
|
{ |
|
int iIndex = m_lookQueue.Find( pTarget ); |
|
if ( iIndex != m_lookQueue.InvalidIndex() ) |
|
{ |
|
m_lookQueue.Remove(iIndex); |
|
} |
|
|
|
iIndex = m_randomLookQueue.Find( pTarget ); |
|
if ( iIndex != m_randomLookQueue.InvalidIndex() ) |
|
{ |
|
m_randomLookQueue.Remove(iIndex); |
|
|
|
// Figure out the new random look time |
|
m_flNextRandomLookTime = gpGlobals->curtime + 1.0; |
|
for (int i = 0; i < m_randomLookQueue.Count(); i++) |
|
{ |
|
if ( m_randomLookQueue[i].m_flEndTime > m_flNextRandomLookTime ) |
|
{ |
|
m_flNextRandomLookTime = m_randomLookQueue[i].m_flEndTime + 0.4; |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Look at other NPCs and clients from time to time |
|
//----------------------------------------------------------------------------- |
|
float CAI_BaseActor::PickLookTarget( bool bExcludePlayers, float minTime, float maxTime ) |
|
{ |
|
return PickLookTarget( m_randomLookQueue, bExcludePlayers, minTime, maxTime ); |
|
} |
|
|
|
float CAI_BaseActor::PickLookTarget( CAI_InterestTarget &queue, bool bExcludePlayers, float minTime, float maxTime ) |
|
{ |
|
AILookTargetArgs_t args; |
|
|
|
args.vTarget = vec3_invalid; |
|
args.flDuration = random->RandomFloat( minTime, maxTime ); |
|
args.flInfluence = random->RandomFloat( 0.3, 0.5 ); |
|
args.flRamp = random->RandomFloat( 0.2, 0.4 ); |
|
args.bExcludePlayers = bExcludePlayers; |
|
args.pQueue = &queue; |
|
|
|
bool foundLookTarget = true; |
|
|
|
if ( !PickTacticalLookTarget( &args ) ) |
|
{ |
|
if ( !PickRandomLookTarget( &args ) ) |
|
{ |
|
foundLookTarget = false; |
|
} |
|
} |
|
|
|
if ( !foundLookTarget ) |
|
{ |
|
// DevMsg("nothing to see\n" ); |
|
MakeRandomLookTarget( &args, minTime, maxTime ); |
|
} |
|
|
|
// See if derived NPCs want to do anything with this look target before I use it |
|
OnSelectedLookTarget( &args ); |
|
|
|
if ( args.hTarget != NULL ) |
|
{ |
|
Assert( args.vTarget == vec3_invalid ); |
|
queue.Add( args.hTarget, args.flInfluence, args.flDuration, args.flRamp ); |
|
} |
|
else |
|
{ |
|
Assert( args.vTarget != vec3_invalid ); |
|
queue.Add( args.vTarget, args.flInfluence, args.flDuration, args.flRamp ); |
|
} |
|
|
|
return args.flDuration; |
|
|
|
} |
|
|
|
bool CAI_BaseActor::PickTacticalLookTarget( AILookTargetArgs_t *pArgs ) |
|
{ |
|
CBaseEntity *pEnemy = GetEnemy(); |
|
|
|
if (pEnemy != NULL) |
|
{ |
|
if ( ( FVisible( pEnemy ) || random->RandomInt(0, 3) == 0 ) && ValidHeadTarget(pEnemy->EyePosition())) |
|
{ |
|
// look at enemy closer |
|
pArgs->hTarget = pEnemy; |
|
pArgs->flInfluence = random->RandomFloat( 0.7, 1.0 ); |
|
pArgs->flRamp = 0; |
|
return true; |
|
} |
|
else |
|
{ |
|
// look at something else for a shorter time |
|
pArgs->flDuration = random->RandomFloat( 0.5, 0.8 ); |
|
// move head faster |
|
pArgs->flRamp = 0.2; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
bool CAI_BaseActor::PickRandomLookTarget( AILookTargetArgs_t *pArgs ) |
|
{ |
|
bool bIsNavigating = ( GetNavigator()->IsGoalActive() && GetNavigator()->IsGoalSet() ); |
|
|
|
if ( bIsNavigating && random->RandomInt(1, 10) <= 3 ) |
|
{ |
|
Vector navLookPoint; |
|
Vector delta; |
|
if ( GetNavigator()->GetPointAlongPath( &navLookPoint, 12 * 12 ) && (delta = navLookPoint - GetAbsOrigin()).Length() > 8.0 * 12.0 ) |
|
{ |
|
if ( random->RandomInt(1, 10) <= 5 ) |
|
{ |
|
pArgs->vTarget = navLookPoint; |
|
pArgs->flDuration = random->RandomFloat( 0.2, 0.4 ); |
|
} |
|
else |
|
{ |
|
pArgs->hTarget = this; |
|
pArgs->flDuration = random->RandomFloat( 1.0, 2.0 ); |
|
} |
|
pArgs->flRamp = 0.2; |
|
return true; |
|
} |
|
} |
|
|
|
if ( GetState() == NPC_STATE_COMBAT && random->RandomInt(1, 10) <= 8 ) |
|
{ |
|
// if in combat, look forward 80% of the time? |
|
pArgs->hTarget = this; |
|
return true; |
|
} |
|
|
|
CBaseEntity *pBestEntity = NULL; |
|
CBaseEntity *pEntity = NULL; |
|
int iHighestImportance = 0; |
|
int iConsidered = 0; |
|
for ( CEntitySphereQuery sphere( GetAbsOrigin(), 30 * 12, 0 ); (pEntity = sphere.GetCurrentEntity()) != NULL; sphere.NextEntity() ) |
|
{ |
|
if (pEntity == this) |
|
{ |
|
continue; |
|
} |
|
|
|
if ( pArgs->bExcludePlayers && pEntity->GetFlags() & FL_CLIENT ) |
|
{ |
|
// Don't look at any players. |
|
continue; |
|
} |
|
|
|
if (!pEntity->IsViewable()) |
|
{ |
|
// Don't look at things without a model, or aren't tagged as interesting |
|
continue; |
|
} |
|
|
|
if ( pEntity->GetOwnerEntity() && !pEntity->GetOwnerEntity()->IsViewable() ) |
|
{ |
|
// Don't look at things that are associated with non-viewable owners. |
|
// Specifically, this prevents NPC's looking at beams or sprites that |
|
// are part of a viewmodel. (sjb) |
|
continue; |
|
} |
|
|
|
// Don't look at any object that is ultimately parented to the player. |
|
// These objects will almost always be at the player's origin (feet), and it |
|
// looks bad when an actor looks at the player's feet. (sjb) |
|
CBaseEntity *pParent = pEntity->GetParent(); |
|
bool bObjectParentedToPlayer = false; |
|
while( pParent ) |
|
{ |
|
if( pParent->IsPlayer() ) |
|
{ |
|
bObjectParentedToPlayer = true; |
|
break; |
|
} |
|
|
|
pParent = pParent->GetParent(); |
|
} |
|
|
|
if( bObjectParentedToPlayer ) |
|
continue; |
|
|
|
// skip entities we're already looking at |
|
if ( pArgs->pQueue->Find( pEntity ) != pArgs->pQueue->InvalidIndex() ) |
|
continue; |
|
|
|
// keep track of number of interesting things |
|
iConsidered++; |
|
|
|
if ((pEntity->GetFlags() & FL_CLIENT) && (pEntity->IsMoving() || random->RandomInt( 0, 2) == 0)) |
|
{ |
|
if (FVisible( pEntity ) && ValidHeadTarget(pEntity->EyePosition())) |
|
{ |
|
pArgs->flDuration = random->RandomFloat( 1.0, 4.0 ); |
|
pBestEntity = pEntity; |
|
break; |
|
} |
|
} |
|
|
|
Vector delta = (pEntity->EyePosition() - EyePosition()); |
|
VectorNormalize( delta ); |
|
|
|
int iImportance; |
|
#if 0 |
|
// consider things in front to be more important than things to the sides |
|
iImportance = (DotProduct( delta, HeadDirection3D() ); |
|
#else |
|
// No, for now, give all targets random priority (as long as they're in front) |
|
iImportance = random->RandomInt( 1, 100 ); |
|
|
|
#endif |
|
// make other npcs, and moving npc's far more important |
|
if (pEntity->MyNPCPointer()) |
|
{ |
|
iImportance *= 10; |
|
if (pEntity->IsMoving()) |
|
{ |
|
iImportance *= 10; |
|
} |
|
} |
|
|
|
if ( iImportance > iHighestImportance ) |
|
{ |
|
if (FVisible( pEntity ) && ValidHeadTarget(pEntity->EyePosition())) |
|
{ |
|
iHighestImportance = iImportance; |
|
pBestEntity = pEntity; |
|
// NDebugOverlay::Line( EyePosition(), pEntity->EyePosition(), 0,0,255, false, 0.5); |
|
} |
|
} |
|
} |
|
|
|
// if there were too few things to look at, don't trust the item |
|
if (iConsidered < random->RandomInt( 0, 5)) |
|
{ |
|
pBestEntity = NULL; |
|
} |
|
|
|
if (pBestEntity) |
|
{ |
|
//Msg("looking at %s\n", pBestEntity->GetClassname() ); |
|
//NDebugOverlay::Line( EyePosition(), pBestEntity->WorldSpaceCenter(), 255, 0, 0, false, 5 ); |
|
pArgs->hTarget = pBestEntity; |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// All attempts to find a target have failed, so just make something up. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseActor::MakeRandomLookTarget( AILookTargetArgs_t *pArgs, float minTime, float maxTime ) |
|
{ |
|
Vector forward, right, up; |
|
GetVectors( &forward, &right, &up ); |
|
|
|
// DevMsg("random view\n"); |
|
|
|
// For now, just look farther afield while driving in the vehicle. Without this we look around wildly! |
|
#ifdef HL2_EPISODIC |
|
if ( MyCombatCharacterPointer() && MyCombatCharacterPointer()->IsInAVehicle() ) |
|
{ |
|
pArgs->vTarget = EyePosition() + forward * 2048 + right * random->RandomFloat(-650,650) + up * random->RandomFloat(-32,32); |
|
} |
|
else |
|
#endif // HL2_EPISODIC |
|
{ |
|
pArgs->vTarget = EyePosition() + forward * 128 + right * random->RandomFloat(-32,32) + up * random->RandomFloat(-16,16); |
|
} |
|
|
|
pArgs->flDuration = random->RandomFloat( minTime, maxTime ); |
|
pArgs->flInfluence = 0.01; |
|
pArgs->flRamp = random->RandomFloat( 0.8, 2.8 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Make sure we're looking at what we're shooting at |
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseActor::StartTaskRangeAttack1( const Task_t *pTask ) |
|
{ |
|
BaseClass::StartTaskRangeAttack1( pTask ); |
|
if (GetEnemy()) |
|
{ |
|
AddLookTarget( GetEnemy(), 1.0, 0.5, 0.2 ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
#pragma region INFO_REMARKABLE polling |
|
// hardcode the default rr_remarkables_enabled value for now because I don't know what |
|
// the current state of unhackable config files is. Should be fixed. |
|
// (TODO) |
|
|
|
#define AI_REMARKABLES_ENABLED_DEFAULT "0" |
|
|
|
|
|
ConVar rr_remarkable_world_entities_replay_limit( "rr_remarkable_world_entities_replay_limit", "1", FCVAR_CHEAT, "TLK_REMARKs will be dispatched no more than this many times for any given info_remarkable" ); |
|
ConVar rr_remarkables_enabled( "rr_remarkables_enabled", AI_REMARKABLES_ENABLED_DEFAULT, FCVAR_CHEAT, "If 1, polling for info_remarkables and issuances of TLK_REMARK is enabled." ); |
|
ConVar rr_remarkable_max_distance( "rr_remarkable_max_distance", "1200", FCVAR_CHEAT, "AIs will not even consider remarkarbles that are more than this many units away." ); |
|
#define AI_REMARK_SPEECH_INTERVAL 1 |
|
/************************************************************************/ |
|
/* TODO: Make perfier |
|
* Plumb through interrupt priority in speech |
|
/************************************************************************/ |
|
bool CAI_BaseActor::UpdateRemarkableSpeech() RESTRICT |
|
{ |
|
// done in caller |
|
/* |
|
if ( !rr_remarkables_enabled.GetBool() ) |
|
return false; |
|
*/ |
|
|
|
VPROF( "CAI_BaseActor::UpdateRemarkableSpeech" ); |
|
if ( CanPollRemarkables() ) |
|
{ |
|
m_fNextRemarkPollTime = gpGlobals->curtime + AI_REMARK_SPEECH_INTERVAL; |
|
|
|
// this is somewhat hokey 12am logic -- we're going to iterate over all |
|
// the remarkables and ask each of them if they're in sight. It's better |
|
// to do this the other way (ie the entities would know when they're being |
|
// looked at) but we don't seem to have a function for that yet. |
|
CInfoRemarkable::tRemarkableList *pList = CInfoRemarkable::GetListOfAllThatIsRemarkable(); |
|
const float maxDistSq = rr_remarkable_max_distance.GetFloat() * rr_remarkable_max_distance.GetFloat(); |
|
const int remarkLimit = rr_remarkable_world_entities_replay_limit.GetInt(); |
|
for ( int i = pList->Head(); pList->IsValidIndex(i); i = pList->Next(i) ) |
|
{ |
|
CInfoRemarkable * RESTRICT remarkable = pList->Element(i); |
|
// do a quick distance test for a rough cull. |
|
if ( GetAbsOrigin().DistToSqr(remarkable->GetAbsOrigin()) > maxDistSq ) |
|
continue; |
|
|
|
if ( remarkable->m_iTimesRemarkedUpon < remarkLimit && |
|
TestRemarkingUpon( remarkable ) ) |
|
{ |
|
// remark upon it |
|
float distToRemarkable = (remarkable->GetAbsOrigin() - GetAbsOrigin()).Length(); |
|
const char *pModifiers = UTIL_VarArgs( "Subject:%s,Distance:%f", remarkable->GetRemarkContext(), distToRemarkable ); |
|
|
|
if ( CanSpeak() && Speak( "TLK_REMARK", pModifiers ) ) |
|
{ |
|
remarkable->m_iTimesRemarkedUpon += 1; |
|
return true; |
|
} |
|
} |
|
} |
|
|
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
bool CAI_BaseActor::CanPollRemarkables() |
|
{ |
|
return m_bRemarkablePolling && |
|
( gpGlobals->curtime > m_fNextRemarkPollTime ) ; |
|
} |
|
|
|
bool CAI_BaseActor::TestRemarkingUpon( CInfoRemarkable * RESTRICT pRemarkable ) |
|
{ |
|
return IsInFieldOfView( pRemarkable ) && |
|
IsLineOfSightClear( pRemarkable, IGNORE_ACTORS ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
#pragma endregion |
|
|
|
|
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Set direction that the NPC is looking |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseActor::AddLookTarget( CBaseEntity *pTarget, float flImportance, float flDuration, float flRamp ) |
|
{ |
|
m_lookQueue.Add( pTarget, flImportance, flDuration, flRamp ); |
|
} |
|
|
|
|
|
void CAI_BaseActor::AddLookTarget( const Vector &vecPosition, float flImportance, float flDuration, float flRamp ) |
|
{ |
|
m_lookQueue.Add( vecPosition, flImportance, flDuration, flRamp ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Maintain eye, head, body postures, etc. |
|
//----------------------------------------------------------------------------- |
|
void CAI_BaseActor::MaintainLookTargets( float flInterval ) |
|
{ |
|
int i; |
|
|
|
if ( m_iszExpressionScene != NULL_STRING && m_hExpressionSceneEnt == NULL ) |
|
{ |
|
InstancedScriptedScene( this, STRING(m_iszExpressionScene), &m_hExpressionSceneEnt, 0.0, true ); |
|
} |
|
|
|
// decay body/spine yaw |
|
m_goalSpineYaw = m_goalSpineYaw * 0.8; |
|
m_goalBodyYaw = m_goalBodyYaw * 0.8; |
|
m_goalHeadCorrection = m_goalHeadCorrection * 0.8; |
|
|
|
// ARRGGHHH, this needs to be moved!!!! |
|
SetAccumulatedYawAndUpdate( ); |
|
ProcessSceneEvents( ); |
|
MaintainTurnActivity( ); |
|
DoBodyLean( ); |
|
UpdateBodyControl( ); |
|
InvalidateBoneCache(); |
|
|
|
// cached versions of the current eye position |
|
Vector vEyePosition = EyePosition( ); |
|
|
|
// FIXME: make this client side and automatic |
|
// set gesture positions |
|
Set( m_ParameterGestureHeight, Get( m_FlexweightGestureUpDown ) ); |
|
Set( m_ParameterGestureWidth, Get( m_FlexweightGestureRightLeft ) ); |
|
|
|
// initialize goal head direction to be current direction - this frames animation layering/pose parameters - |
|
// but with the head controlls removed. |
|
Vector vHead = HeadDirection3D( ); |
|
float flHeadInfluence = 0.0; |
|
|
|
// NDebugOverlay::Line( vEyePosition, vEyePosition + vHead * 16, 0,0,255, false, 0.1); |
|
|
|
// clean up look targets |
|
m_lookQueue.Cleanup(); |
|
|
|
// clean up random look targets |
|
m_randomLookQueue.Cleanup(); |
|
|
|
// clean up synthetic look targets |
|
m_syntheticLookQueue.Cleanup(); |
|
|
|
// if there's real things to look at, turn off the random targets |
|
if (m_lookQueue.Count() != 0 || m_syntheticLookQueue.Count() != 0) |
|
{ |
|
for (i = 0; i < m_randomLookQueue.Count(); i++) |
|
{ |
|
if (gpGlobals->curtime < m_randomLookQueue[i].m_flEndTime - m_randomLookQueue[i].m_flRamp - 0.2) |
|
{ |
|
m_randomLookQueue[i].m_flEndTime = gpGlobals->curtime + m_randomLookQueue[i].m_flRamp + 0.2; |
|
} |
|
} |
|
m_flNextRandomLookTime = gpGlobals->curtime + 1.0; |
|
} |
|
else if (gpGlobals->curtime >= m_flNextRandomLookTime && GetState() != NPC_STATE_SCRIPT) |
|
{ |
|
// Look at whatever! |
|
m_flNextRandomLookTime = gpGlobals->curtime + PickLookTarget( m_randomLookQueue ) - 0.4; |
|
} |
|
|
|
// don't bother with any of the rest if the player can't see you |
|
if ( CanSkipAnimation() ) |
|
{ |
|
return; |
|
} |
|
|
|
// Time to finish the current random expression? Or time to pick a new one? |
|
if ( m_flNextRandomExpressionTime && gpGlobals->curtime > m_flNextRandomExpressionTime ) |
|
{ |
|
// Random expressions need to be cleared, because they don't loop. So if we |
|
// pick the same one again, we want to restart it. |
|
ClearExpression(); |
|
|
|
PlayExpressionForState( GetState() ); |
|
} |
|
|
|
CUtlVector<CAI_InterestTarget_t *> active; |
|
// clean up random look targets |
|
for (i = 0; i < m_randomLookQueue.Count(); i++) |
|
{ |
|
active.AddToTail( &m_randomLookQueue[i] ); |
|
} |
|
for (i = 0; i < m_lookQueue.Count(); i++) |
|
{ |
|
active.AddToTail( &m_lookQueue[i] ); |
|
} |
|
for (i = 0; i < m_syntheticLookQueue.Count(); i++) |
|
{ |
|
active.AddToTail( &m_syntheticLookQueue[i] ); |
|
} |
|
|
|
// figure out ideal head yaw |
|
bool bValidHeadTarget = false; |
|
bool bExpectedHeadTarget = false; |
|
for (i = 0; i < active.Count();i++) |
|
{ |
|
Vector dir; |
|
float flDist = 100.0f; |
|
|
|
bExpectedHeadTarget = true; |
|
float flInterest = active[i]->Interest( ); |
|
|
|
if (active[i]->IsThis( this )) |
|
{ |
|
int iForward = LookupAttachment( "forward" ); |
|
if (iForward) |
|
{ |
|
Vector tmp1; |
|
GetAttachment( iForward, tmp1, &dir, NULL, NULL ); |
|
} |
|
else |
|
{ |
|
dir = HeadDirection3D(); |
|
} |
|
} |
|
else |
|
{ |
|
dir = active[i]->GetPosition() - vEyePosition; |
|
flDist = VectorNormalize( dir ); |
|
flInterest = flInterest * HeadTargetValidity( active[i]->GetPosition() ); |
|
} |
|
|
|
/* |
|
if ( (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) ) |
|
{ |
|
DevMsg( "head (%d) %.2f : %s : %.1f %.1f %.1f\n", i, flInterest, active[i]->m_hTarget->GetClassname(), active[i]->GetPosition().x, active[i]->GetPosition().y, active[i]->GetPosition().z ); |
|
} |
|
*/ |
|
|
|
if (flInterest > 0.0) |
|
{ |
|
if (flHeadInfluence == 0.0) |
|
{ |
|
vHead = dir; |
|
flHeadInfluence = flInterest; |
|
} |
|
else |
|
{ |
|
flHeadInfluence = flHeadInfluence * (1 - flInterest) + flInterest; |
|
float w = flInterest / flHeadInfluence; |
|
vHead = vHead * (1 - w) + dir * w; |
|
} |
|
|
|
bValidHeadTarget = true; |
|
|
|
// NDebugOverlay::Line( vEyePosition, vEyePosition + dir * 64, 0,255,0, false, 0.1); |
|
} |
|
else |
|
{ |
|
// NDebugOverlay::Line( vEyePosition, active[i]->GetPosition(), 255,0,0, false, 0.1); |
|
} |
|
} |
|
|
|
Assert( flHeadInfluence <= 1.0 ); |
|
|
|
// turn head toward target |
|
if (bValidHeadTarget) |
|
{ |
|
UpdateHeadControl( vEyePosition + vHead * 100, flHeadInfluence ); |
|
m_goalHeadDirection = vHead; |
|
m_goalHeadInfluence = flHeadInfluence; |
|
} |
|
else |
|
{ |
|
// no target, decay all head control direction |
|
m_goalHeadDirection = m_goalHeadDirection * 0.8 + vHead * 0.2; |
|
|
|
m_goalHeadInfluence = MAX( m_goalHeadInfluence - 0.2, 0 ); |
|
|
|
VectorNormalize( m_goalHeadDirection ); |
|
UpdateHeadControl( vEyePosition + m_goalHeadDirection * 100, m_goalHeadInfluence ); |
|
// NDebugOverlay::Line( vEyePosition, vEyePosition + m_goalHeadDirection * 100, 255,0,0, false, 0.1); |
|
} |
|
|
|
// DevMsg( "%.1f %.1f ", GetPoseParameter( "head_pitch" ), GetPoseParameter( "head_roll" ) ); |
|
|
|
// figure out eye target |
|
// eyes need to look directly at a target, even if the head doesn't quite aim there yet. |
|
bool bFoundTarget = false; |
|
EHANDLE hTarget = NULL; |
|
|
|
for (i = active.Count() - 1; i >= 0; i--) |
|
{ |
|
if (active[i]->IsThis( this )) |
|
{ |
|
// DevMsg( "eyes (%d) %s\n", i, STRING( active[i]->m_hTarget->GetEntityName().ToCStr() ) ); |
|
bFoundTarget = true; |
|
hTarget = this; |
|
SetViewtarget( vEyePosition + HeadDirection3D() * 100 ); |
|
// NDebugOverlay::Line( vEyePosition, vEyePosition + HeadDirection3D() * 100, 255,0,0, false, 0.1); |
|
break; |
|
} |
|
else |
|
{ |
|
// E3 Hack |
|
if (ValidEyeTarget(active[i]->GetPosition())) |
|
{ |
|
// DevMsg( "eyes (%d) %s\n", i, STRING( pTarget->GetEntityName().ToCStr() ) ); |
|
|
|
bFoundTarget = true; |
|
hTarget = active[i]->m_hTarget; |
|
SetViewtarget( active[i]->GetPosition() ); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
// FIXME: add blink when changing targets |
|
if (m_hLookTarget != hTarget) |
|
{ |
|
m_flBlinktime -= 0.5; |
|
m_hLookTarget = hTarget; |
|
|
|
if ( (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) && ai_debug_looktargets.GetInt() == 2 && m_hLookTarget.Get() ) |
|
{ |
|
if ( m_hLookTarget != this ) |
|
{ |
|
Vector vecEyePos = m_hLookTarget->EyePosition(); |
|
NDebugOverlay::Box( vecEyePos, -Vector(5,5,5), Vector(5,5,5), 0, 255, 0, 255, 20 ); |
|
NDebugOverlay::Line( EyePosition(), vecEyePos, 0,255,0, true, 20 ); |
|
NDebugOverlay::Text( vecEyePos, UTIL_VarArgs( "%s (%s)", m_hLookTarget->GetClassname(), m_hLookTarget->GetDebugName() ), false, 20 ); |
|
} |
|
} |
|
|
|
OnNewLookTarget(); |
|
} |
|
|
|
// this should take into acount where it will try to be.... |
|
if (!bFoundTarget && !ValidEyeTarget( GetViewtarget() )) |
|
{ |
|
Vector right, up; |
|
VectorVectors( HeadDirection3D(), right, up ); |
|
// DevMsg("random view\n"); |
|
SetViewtarget( EyePosition() + HeadDirection3D() * 128 + right * random->RandomFloat(-32,32) + up * random->RandomFloat(-16,16) ); |
|
} |
|
|
|
if ( m_hLookTarget != NULL ) |
|
{ |
|
Vector absVel = m_hLookTarget->GetAbsVelocity(); |
|
CBaseEntity *ground = m_hLookTarget->GetGroundEntity(); |
|
if ( ground && ground->GetMoveType() == MOVETYPE_PUSH) |
|
{ |
|
absVel = absVel + ground->GetAbsVelocity(); |
|
} |
|
|
|
#ifdef HL2_EPISODIC |
|
// Translate our position if riding in a vehicle |
|
if ( m_hLookTarget->MyCombatCharacterPointer() ) |
|
{ |
|
CBaseCombatCharacter *pBCC = m_hLookTarget->MyCombatCharacterPointer(); |
|
CBaseEntity *pVehicle = pBCC->GetVehicleEntity(); |
|
if ( pVehicle ) |
|
{ |
|
IPhysicsObject *pObj = pVehicle->VPhysicsGetObject(); |
|
if ( pObj ) |
|
{ |
|
Vector vecVelocity; |
|
pObj->GetVelocity( &vecVelocity, NULL ); |
|
|
|
absVel += vecVelocity; |
|
} |
|
} |
|
} |
|
#endif //HL2_EPISODIC |
|
|
|
if ( !VectorCompare( absVel, vec3_origin ) ) |
|
{ |
|
Vector viewTarget = GetViewtarget(); |
|
|
|
// Forward one think cycle |
|
viewTarget += absVel * flInterval; |
|
|
|
SetViewtarget( viewTarget ); |
|
} |
|
} |
|
|
|
// NDebugOverlay::Triangle( vEyePosition, GetViewtarget(), GetAbsOrigin(), 255, 255, 255, 10, false, 0.1 ); |
|
|
|
// DevMsg("pitch %.1f yaw %.1f\n", GetFlexWeight( "eyes_updown" ), GetFlexWeight( "eyes_rightleft" ) ); |
|
|
|
// blink |
|
if (m_flBlinktime < gpGlobals->curtime) |
|
{ |
|
Blink(); |
|
m_flBlinktime = gpGlobals->curtime + random->RandomFloat( 1.5, 4.5 ); |
|
} |
|
|
|
if ( ai_debug_looktargets.GetInt() == 1 && (m_debugOverlays & OVERLAY_NPC_SELECTED_BIT) ) |
|
{ |
|
NDebugOverlay::Box( GetViewtarget(), -Vector(2,2,2), Vector(2,2,2), 0, 255, 0, 0, 20 ); |
|
NDebugOverlay::Line( EyePosition(),GetViewtarget(), 0,255,0, false, .1 ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseActor::PlayExpressionForState( NPC_STATE state ) |
|
{ |
|
// If we have an override expression, use it above everything else |
|
if ( m_iszExpressionOverride != NULL_STRING && state != NPC_STATE_DEAD ) |
|
{ |
|
SetExpression( STRING(m_iszExpressionOverride) ); |
|
return; |
|
} |
|
|
|
// If we have a random expression, use that |
|
const char *pszExpression = SelectRandomExpressionForState( state ); |
|
if ( pszExpression && *pszExpression ) |
|
{ |
|
float flDuration = SetExpression( pszExpression ); |
|
m_flNextRandomExpressionTime = gpGlobals->curtime + flDuration; |
|
return; |
|
} |
|
else |
|
{ |
|
// Stop looking for random expressions for this state |
|
m_flNextRandomExpressionTime = 0; |
|
} |
|
|
|
// Lastly, use the base expression loops |
|
switch ( state ) |
|
{ |
|
case NPC_STATE_IDLE: |
|
if ( m_iszIdleExpression != NULL_STRING ) |
|
{ |
|
SetExpression( STRING(m_iszIdleExpression) ); |
|
} |
|
break; |
|
|
|
case NPC_STATE_COMBAT: |
|
if ( m_iszCombatExpression != NULL_STRING ) |
|
{ |
|
SetExpression( STRING(m_iszCombatExpression) ); |
|
} |
|
break; |
|
|
|
case NPC_STATE_ALERT: |
|
if ( m_iszAlertExpression != NULL_STRING ) |
|
{ |
|
SetExpression( STRING(m_iszAlertExpression) ); |
|
} |
|
break; |
|
|
|
case NPC_STATE_PLAYDEAD: |
|
case NPC_STATE_DEAD: |
|
if ( m_iszDeathExpression != NULL_STRING ) |
|
{ |
|
SetExpression( STRING(m_iszDeathExpression) ); |
|
} |
|
break; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Return a random expression for the specified state to play over |
|
// the state's expression loop. |
|
//----------------------------------------------------------------------------- |
|
const char *CAI_BaseActor::SelectRandomExpressionForState( NPC_STATE state ) |
|
{ |
|
return NULL; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseActor::OnStateChange( NPC_STATE OldState, NPC_STATE NewState ) |
|
{ |
|
PlayExpressionForState( NewState ); |
|
|
|
#ifdef HL2_EPISODIC |
|
// If we've just switched states, ensure we stop any scenes that asked to be stopped |
|
if ( OldState == NPC_STATE_IDLE ) |
|
{ |
|
RemoveActorFromScriptedScenes( this, true, true ); |
|
} |
|
#endif |
|
|
|
BaseClass::OnStateChange( OldState, NewState ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
float CAI_BaseActor::SetExpression( const char *pszExpressionScene ) |
|
{ |
|
if ( !pszExpressionScene || !*pszExpressionScene ) |
|
{ |
|
ClearExpression(); |
|
return 0; |
|
} |
|
|
|
if ( m_iszExpressionScene != NULL_STRING && stricmp( STRING(m_iszExpressionScene), pszExpressionScene ) == 0 ) |
|
{ |
|
return 0; |
|
} |
|
|
|
if ( m_hExpressionSceneEnt != NULL ) |
|
{ |
|
StopScriptedScene( this, m_hExpressionSceneEnt ); |
|
} |
|
|
|
if ( ai_debug_expressions.GetInt() ) |
|
{ |
|
Msg("%s (%s) set expression to: %s\n", GetClassname(), GetDebugName(), pszExpressionScene ); |
|
} |
|
|
|
m_iszExpressionScene = NULL_STRING; |
|
if ( pszExpressionScene ) |
|
{ |
|
float flDuration = InstancedScriptedScene( this, pszExpressionScene, &m_hExpressionSceneEnt, 0.0, true ); |
|
|
|
if ( m_hExpressionSceneEnt != NULL ) |
|
{ |
|
m_iszExpressionScene = AllocPooledString( pszExpressionScene ); |
|
} |
|
|
|
return flDuration; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseActor::ClearExpression() |
|
{ |
|
if ( m_hExpressionSceneEnt != NULL ) |
|
{ |
|
StopScriptedScene( this, m_hExpressionSceneEnt ); |
|
} |
|
m_iszExpressionScene = NULL_STRING; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
const char *CAI_BaseActor::GetExpression() |
|
{ |
|
return STRING(m_iszExpressionScene); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
void CAI_BaseActor::InputSetExpressionOverride( inputdata_t &inputdata ) |
|
{ |
|
bool fHadOverride = ( m_iszExpressionOverride != NULL_STRING ); |
|
m_iszExpressionOverride = inputdata.value.StringID(); |
|
if ( m_iszExpressionOverride != NULL_STRING ) |
|
{ |
|
SetExpression( STRING(m_iszExpressionOverride) ); |
|
} |
|
else if ( fHadOverride ) |
|
{ |
|
PlayExpressionForState( GetState() ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseActor::UseSemaphore( void ) |
|
{ |
|
if ( m_bDontUseSemaphore ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
CAI_Expresser *CAI_BaseActor::CreateExpresser() |
|
{ |
|
m_pExpresser = new CAI_Expresser(this); |
|
return m_pExpresser; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
CAI_Expresser *CAI_BaseActor::GetExpresser() |
|
{ |
|
return m_pExpresser; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
|
|
bool CAI_BaseActor::CreateComponents() |
|
{ |
|
if ( !BaseClass::CreateComponents() ) |
|
return false; |
|
|
|
m_pExpresser = CreateExpresser(); |
|
if ( !m_pExpresser) |
|
return false; |
|
|
|
m_pExpresser->Connect(this); |
|
|
|
return true; |
|
} |
|
|
|
void CAI_BaseActor::GatherConditions( void ) |
|
{ |
|
BaseClass::GatherConditions(); |
|
if ( rr_remarkables_enabled.GetBool() ) |
|
{ |
|
UpdateRemarkableSpeech(); |
|
} |
|
} |
|
//-----------------------------------------------------------------------------
|
|
|