mirror of
https://github.com/nillerusr/source-engine.git
synced 2024-12-22 22:27:05 +00:00
1102 lines
28 KiB
C++
1102 lines
28 KiB
C++
//========= Copyright Valve Corporation, All rights reserved. ============//
|
|
//
|
|
// Purpose:
|
|
//
|
|
//=============================================================================//
|
|
|
|
#include "cbase.h"
|
|
#include "ammodef.h"
|
|
#include "AI_Hint.h"
|
|
#include "AI_Navigator.h"
|
|
#include "npc_Assassin.h"
|
|
#include "game.h"
|
|
#include "npcevent.h"
|
|
#include "engine/IEngineSound.h"
|
|
#include "ai_squad.h"
|
|
#include "AI_SquadSlot.h"
|
|
#include "ai_moveprobe.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
ConVar sk_assassin_health( "sk_assassin_health","150");
|
|
ConVar g_debug_assassin( "g_debug_assassin", "0" );
|
|
|
|
//=========================================================
|
|
// Anim Events
|
|
//=========================================================
|
|
#define ASSASSIN_AE_FIRE_PISTOL_RIGHT 1
|
|
#define ASSASSIN_AE_FIRE_PISTOL_LEFT 2
|
|
#define ASSASSIN_AE_KICK_HIT 3
|
|
|
|
int AE_ASSASIN_FIRE_PISTOL_RIGHT;
|
|
int AE_ASSASIN_FIRE_PISTOL_LEFT;
|
|
int AE_ASSASIN_KICK_HIT;
|
|
|
|
//=========================================================
|
|
// Assassin activities
|
|
//=========================================================
|
|
int ACT_ASSASSIN_FLIP_LEFT;
|
|
int ACT_ASSASSIN_FLIP_RIGHT;
|
|
int ACT_ASSASSIN_FLIP_BACK;
|
|
int ACT_ASSASSIN_FLIP_FORWARD;
|
|
int ACT_ASSASSIN_PERCH;
|
|
|
|
//=========================================================
|
|
// Flip types
|
|
//=========================================================
|
|
enum
|
|
{
|
|
FLIP_LEFT,
|
|
FLIP_RIGHT,
|
|
FLIP_FORWARD,
|
|
FLIP_BACKWARD,
|
|
NUM_FLIP_TYPES,
|
|
};
|
|
|
|
//=========================================================
|
|
// Private conditions
|
|
//=========================================================
|
|
enum Assassin_Conds
|
|
{
|
|
COND_ASSASSIN_ENEMY_TARGETTING_ME = LAST_SHARED_CONDITION,
|
|
};
|
|
|
|
//=========================================================
|
|
// Assassin schedules
|
|
//=========================================================
|
|
enum
|
|
{
|
|
SCHED_ASSASSIN_FIND_VANTAGE_POINT = LAST_SHARED_SCHEDULE,
|
|
SCHED_ASSASSIN_EVADE,
|
|
SCHED_ASSASSIN_STALK_ENEMY,
|
|
SCHED_ASSASSIN_LUNGE,
|
|
};
|
|
|
|
//=========================================================
|
|
// Assassin tasks
|
|
//=========================================================
|
|
enum
|
|
{
|
|
TASK_ASSASSIN_GET_PATH_TO_VANTAGE_POINT = LAST_SHARED_TASK,
|
|
TASK_ASSASSIN_EVADE,
|
|
TASK_ASSASSIN_SET_EYE_STATE,
|
|
TASK_ASSASSIN_LUNGE,
|
|
};
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Class Constructor
|
|
//-----------------------------------------------------------------------------
|
|
CNPC_Assassin::CNPC_Assassin( void )
|
|
{
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
LINK_ENTITY_TO_CLASS( npc_assassin, CNPC_Assassin );
|
|
|
|
#if 0
|
|
//---------------------------------------------------------
|
|
// Custom Client entity
|
|
//---------------------------------------------------------
|
|
IMPLEMENT_SERVERCLASS_ST(CNPC_Assassin, DT_NPC_Assassin)
|
|
END_SEND_TABLE()
|
|
|
|
#endif
|
|
|
|
//---------------------------------------------------------
|
|
// Save/Restore
|
|
//---------------------------------------------------------
|
|
BEGIN_DATADESC( CNPC_Assassin )
|
|
DEFINE_FIELD( m_nNumFlips, FIELD_INTEGER ),
|
|
DEFINE_FIELD( m_nLastFlipType, FIELD_INTEGER ),
|
|
DEFINE_FIELD( m_flNextFlipTime, FIELD_TIME ),
|
|
DEFINE_FIELD( m_flNextLungeTime, FIELD_TIME ),
|
|
DEFINE_FIELD( m_flNextShotTime, FIELD_TIME ),
|
|
DEFINE_FIELD( m_bEvade, FIELD_BOOLEAN ),
|
|
DEFINE_FIELD( m_bAggressive, FIELD_BOOLEAN ),
|
|
DEFINE_FIELD( m_bBlinkState, FIELD_BOOLEAN ),
|
|
DEFINE_FIELD( m_pEyeSprite, FIELD_CLASSPTR ),
|
|
DEFINE_FIELD( m_pEyeTrail, FIELD_CLASSPTR ),
|
|
END_DATADESC()
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
void CNPC_Assassin::Precache( void )
|
|
{
|
|
PrecacheModel( "models/fassassin.mdl" );
|
|
|
|
PrecacheScriptSound( "NPC_Assassin.ShootPistol" );
|
|
PrecacheScriptSound( "Zombie.AttackHit" );
|
|
PrecacheScriptSound( "Assassin.AttackMiss" );
|
|
PrecacheScriptSound( "NPC_Assassin.Footstep" );
|
|
|
|
PrecacheModel( "sprites/redglow1.vmt" );
|
|
|
|
BaseClass::Precache();
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
void CNPC_Assassin::Spawn( void )
|
|
{
|
|
Precache();
|
|
|
|
SetModel( "models/fassassin.mdl" );
|
|
|
|
SetHullType(HULL_HUMAN);
|
|
SetHullSizeNormal();
|
|
|
|
SetSolid( SOLID_BBOX );
|
|
AddSolidFlags( FSOLID_NOT_STANDABLE );
|
|
SetMoveType( MOVETYPE_STEP );
|
|
SetBloodColor( BLOOD_COLOR_RED );
|
|
|
|
m_iHealth = sk_assassin_health.GetFloat();
|
|
m_flFieldOfView = 0.1;
|
|
m_NPCState = NPC_STATE_NONE;
|
|
|
|
CapabilitiesClear();
|
|
CapabilitiesAdd( bits_CAP_MOVE_CLIMB | bits_CAP_MOVE_GROUND | bits_CAP_MOVE_JUMP );
|
|
CapabilitiesAdd( bits_CAP_SQUAD | bits_CAP_USE_WEAPONS | bits_CAP_AIM_GUN | bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_INNATE_RANGE_ATTACK2 | bits_CAP_INNATE_MELEE_ATTACK1 );
|
|
|
|
//Turn on our guns
|
|
SetBodygroup( 1, 1 );
|
|
|
|
int attachment = LookupAttachment( "Eye" );
|
|
|
|
// Start up the eye glow
|
|
m_pEyeSprite = CSprite::SpriteCreate( "sprites/redglow1.vmt", GetLocalOrigin(), false );
|
|
|
|
if ( m_pEyeSprite != NULL )
|
|
{
|
|
m_pEyeSprite->SetAttachment( this, attachment );
|
|
m_pEyeSprite->SetTransparency( kRenderTransAdd, 255, 255, 255, 200, kRenderFxNone );
|
|
m_pEyeSprite->SetScale( 0.25f );
|
|
}
|
|
|
|
// Start up the eye trail
|
|
m_pEyeTrail = CSpriteTrail::SpriteTrailCreate( "sprites/bluelaser1.vmt", GetLocalOrigin(), false );
|
|
|
|
if ( m_pEyeTrail != NULL )
|
|
{
|
|
m_pEyeTrail->SetAttachment( this, attachment );
|
|
m_pEyeTrail->SetTransparency( kRenderTransAdd, 255, 0, 0, 200, kRenderFxNone );
|
|
m_pEyeTrail->SetStartWidth( 8.0f );
|
|
m_pEyeTrail->SetLifeTime( 0.75f );
|
|
}
|
|
|
|
NPCInit();
|
|
|
|
m_bEvade = false;
|
|
m_bAggressive = false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Returns true if a reasonable jumping distance
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
bool CNPC_Assassin::IsJumpLegal(const Vector &startPos, const Vector &apex, const Vector &endPos) const
|
|
{
|
|
const float MAX_JUMP_RISE = 256.0f;
|
|
const float MAX_JUMP_DISTANCE = 256.0f;
|
|
const float MAX_JUMP_DROP = 512.0f;
|
|
|
|
return BaseClass::IsJumpLegal( startPos, apex, endPos, MAX_JUMP_RISE, MAX_JUMP_DROP, MAX_JUMP_DISTANCE );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : flDot -
|
|
// flDist -
|
|
// Output : int CNPC_Assassin::MeleeAttack1Conditions
|
|
//-----------------------------------------------------------------------------
|
|
int CNPC_Assassin::MeleeAttack1Conditions ( float flDot, float flDist )
|
|
{
|
|
if ( flDist > 84 )
|
|
return COND_TOO_FAR_TO_ATTACK;
|
|
|
|
if ( flDot < 0.7f )
|
|
return 0;
|
|
|
|
if ( GetEnemy() == NULL )
|
|
return 0;
|
|
|
|
return COND_CAN_MELEE_ATTACK1;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : flDot -
|
|
// flDist -
|
|
// Output : int CNPC_Assassin::RangeAttack1Conditions
|
|
//-----------------------------------------------------------------------------
|
|
int CNPC_Assassin::RangeAttack1Conditions ( float flDot, float flDist )
|
|
{
|
|
if ( flDist < 84 )
|
|
return COND_TOO_CLOSE_TO_ATTACK;
|
|
|
|
if ( flDist > 1024 )
|
|
return COND_TOO_FAR_TO_ATTACK;
|
|
|
|
if ( flDot < 0.5f )
|
|
return COND_NOT_FACING_ATTACK;
|
|
|
|
return COND_CAN_RANGE_ATTACK1;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : flDot -
|
|
// flDist -
|
|
// Output : int CNPC_Assassin::RangeAttack1Conditions
|
|
//-----------------------------------------------------------------------------
|
|
int CNPC_Assassin::RangeAttack2Conditions ( float flDot, float flDist )
|
|
{
|
|
if ( m_flNextLungeTime > gpGlobals->curtime )
|
|
return 0;
|
|
|
|
float lungeRange = GetSequenceMoveDist( SelectWeightedSequence( (Activity) ACT_ASSASSIN_FLIP_FORWARD ) );
|
|
|
|
if ( flDist < lungeRange * 0.25f )
|
|
return COND_TOO_CLOSE_TO_ATTACK;
|
|
|
|
if ( flDist > lungeRange * 1.5f )
|
|
return COND_TOO_FAR_TO_ATTACK;
|
|
|
|
if ( flDot < 0.75f )
|
|
return COND_NOT_FACING_ATTACK;
|
|
|
|
if ( GetEnemy() == NULL )
|
|
return 0;
|
|
|
|
// Check for a clear path
|
|
trace_t tr;
|
|
UTIL_TraceHull( GetAbsOrigin(), GetEnemy()->GetAbsOrigin(), GetHullMins(), GetHullMaxs(), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr );
|
|
|
|
if ( tr.fraction == 1.0f || tr.m_pEnt == GetEnemy() )
|
|
return COND_CAN_RANGE_ATTACK2;
|
|
|
|
return 0;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : hand -
|
|
//-----------------------------------------------------------------------------
|
|
void CNPC_Assassin::FirePistol( int hand )
|
|
{
|
|
if ( m_flNextShotTime > gpGlobals->curtime )
|
|
return;
|
|
|
|
m_flNextShotTime = gpGlobals->curtime + random->RandomFloat( 0.05f, 0.15f );
|
|
|
|
Vector muzzlePos;
|
|
QAngle muzzleAngle;
|
|
|
|
const char *handName = ( hand ) ? "LeftMuzzle" : "RightMuzzle";
|
|
|
|
GetAttachment( handName, muzzlePos, muzzleAngle );
|
|
|
|
Vector muzzleDir;
|
|
|
|
if ( GetEnemy() == NULL )
|
|
{
|
|
AngleVectors( muzzleAngle, &muzzleDir );
|
|
}
|
|
else
|
|
{
|
|
muzzleDir = GetEnemy()->BodyTarget( muzzlePos ) - muzzlePos;
|
|
VectorNormalize( muzzleDir );
|
|
}
|
|
|
|
int bulletType = GetAmmoDef()->Index( "Pistol" );
|
|
|
|
FireBullets( 1, muzzlePos, muzzleDir, VECTOR_CONE_5DEGREES, 1024, bulletType, 2 );
|
|
|
|
UTIL_MuzzleFlash( muzzlePos, muzzleAngle, 0.5f, 1 );
|
|
|
|
CPASAttenuationFilter filter( this );
|
|
EmitSound( filter, entindex(), "NPC_Assassin.ShootPistol" );
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
void CNPC_Assassin::HandleAnimEvent( animevent_t *pEvent )
|
|
{
|
|
|
|
if ( pEvent->event == AE_ASSASIN_FIRE_PISTOL_RIGHT )
|
|
{
|
|
FirePistol( 0 );
|
|
return;
|
|
}
|
|
|
|
if ( pEvent->event == AE_ASSASIN_FIRE_PISTOL_LEFT )
|
|
{
|
|
FirePistol( 1 );
|
|
return;
|
|
}
|
|
|
|
if ( pEvent->event == AE_ASSASIN_KICK_HIT )
|
|
{
|
|
Vector attackDir = BodyDirection2D();
|
|
Vector attackPos = WorldSpaceCenter() + ( attackDir * 64.0f );
|
|
|
|
trace_t tr;
|
|
UTIL_TraceHull( WorldSpaceCenter(), attackPos, -Vector(8,8,8), Vector(8,8,8), MASK_SHOT_HULL, this, COLLISION_GROUP_NONE, &tr );
|
|
|
|
if ( ( tr.m_pEnt != NULL ) && ( tr.DidHitWorld() == false ) )
|
|
{
|
|
if ( tr.m_pEnt->m_takedamage != DAMAGE_NO )
|
|
{
|
|
CTakeDamageInfo info( this, this, 5, DMG_CLUB );
|
|
CalculateMeleeDamageForce( &info, (tr.endpos - tr.startpos), tr.endpos );
|
|
tr.m_pEnt->TakeDamage( info );
|
|
|
|
CBasePlayer *pPlayer = ToBasePlayer( tr.m_pEnt );
|
|
|
|
if ( pPlayer != NULL )
|
|
{
|
|
//Kick the player angles
|
|
pPlayer->ViewPunch( QAngle( -30, 40, 10 ) );
|
|
}
|
|
|
|
EmitSound( "Zombie.AttackHit" );
|
|
//EmitSound( "Assassin.AttackHit" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
EmitSound( "Assassin.AttackMiss" );
|
|
//EmitSound( "Assassin.AttackMiss" );
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
BaseClass::HandleAnimEvent( pEvent );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Causes the assassin to prefer to run away, rather than towards her target
|
|
//-----------------------------------------------------------------------------
|
|
bool CNPC_Assassin::MovementCost( int moveType, const Vector &vecStart, const Vector &vecEnd, float *pCost )
|
|
{
|
|
if ( GetEnemy() == NULL )
|
|
return true;
|
|
|
|
float multiplier = 1.0f;
|
|
|
|
Vector moveDir = ( vecEnd - vecStart );
|
|
VectorNormalize( moveDir );
|
|
|
|
Vector enemyDir = ( GetEnemy()->GetAbsOrigin() - vecStart );
|
|
VectorNormalize( enemyDir );
|
|
|
|
// If we're moving towards our enemy, then the cost is much higher than normal
|
|
if ( DotProduct( enemyDir, moveDir ) > 0.5f )
|
|
{
|
|
multiplier = 16.0f;
|
|
}
|
|
|
|
*pCost *= multiplier;
|
|
|
|
return ( multiplier != 1 );
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
int CNPC_Assassin::SelectSchedule ( void )
|
|
{
|
|
switch ( m_NPCState )
|
|
{
|
|
case NPC_STATE_IDLE:
|
|
case NPC_STATE_ALERT:
|
|
{
|
|
if ( HasCondition ( COND_HEAR_DANGER ) )
|
|
return SCHED_TAKE_COVER_FROM_BEST_SOUND;
|
|
|
|
if ( HasCondition ( COND_HEAR_COMBAT ) )
|
|
return SCHED_INVESTIGATE_SOUND;
|
|
}
|
|
break;
|
|
|
|
case NPC_STATE_COMBAT:
|
|
{
|
|
// dead enemy
|
|
if ( HasCondition( COND_ENEMY_DEAD ) )
|
|
{
|
|
// call base class, all code to handle dead enemies is centralized there.
|
|
return BaseClass::SelectSchedule();
|
|
}
|
|
|
|
// Need to move
|
|
if ( /*( HasCondition( COND_SEE_ENEMY ) && HasCondition( COND_ASSASSIN_ENEMY_TARGETTING_ME ) && random->RandomInt( 0, 32 ) == 0 && m_flNextFlipTime < gpGlobals->curtime ) )*/
|
|
( m_nNumFlips > 0 ) ||
|
|
( ( HasCondition ( COND_LIGHT_DAMAGE ) && random->RandomInt( 0, 2 ) == 0 ) ) || ( HasCondition ( COND_HEAVY_DAMAGE ) ) )
|
|
{
|
|
if ( m_nNumFlips <= 0 )
|
|
{
|
|
m_nNumFlips = random->RandomInt( 1, 2 );
|
|
}
|
|
|
|
return SCHED_ASSASSIN_EVADE;
|
|
}
|
|
|
|
// Can kick
|
|
if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) )
|
|
return SCHED_MELEE_ATTACK1;
|
|
|
|
// Can shoot
|
|
if ( HasCondition( COND_CAN_RANGE_ATTACK2 ) )
|
|
{
|
|
m_flNextLungeTime = gpGlobals->curtime + 2.0f;
|
|
m_nLastFlipType = FLIP_FORWARD;
|
|
|
|
return SCHED_ASSASSIN_LUNGE;
|
|
}
|
|
|
|
// Can shoot
|
|
if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) )
|
|
return SCHED_RANGE_ATTACK1;
|
|
|
|
// Face our enemy
|
|
if ( HasCondition( COND_SEE_ENEMY ) )
|
|
return SCHED_COMBAT_FACE;
|
|
|
|
// new enemy
|
|
if ( HasCondition( COND_NEW_ENEMY ) )
|
|
return SCHED_TAKE_COVER_FROM_ENEMY;
|
|
|
|
// ALERT( at_console, "stand\n");
|
|
return SCHED_ASSASSIN_FIND_VANTAGE_POINT;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return BaseClass::SelectSchedule();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CNPC_Assassin::PrescheduleThink( void )
|
|
{
|
|
if ( GetActivity() == ACT_RUN || GetActivity() == ACT_WALK)
|
|
{
|
|
CPASAttenuationFilter filter( this );
|
|
|
|
static int iStep = 0;
|
|
iStep = ! iStep;
|
|
if (iStep)
|
|
{
|
|
EmitSound( filter, entindex(), "NPC_Assassin.Footstep" );
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : right -
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CNPC_Assassin::CanFlip( int flipType, Activity &activity, const Vector *avoidPosition )
|
|
{
|
|
Vector testDir;
|
|
Activity act = ACT_INVALID;
|
|
|
|
switch( flipType )
|
|
{
|
|
case FLIP_RIGHT:
|
|
GetVectors( NULL, &testDir, NULL );
|
|
act = NPC_TranslateActivity( (Activity) ACT_ASSASSIN_FLIP_RIGHT );
|
|
break;
|
|
|
|
case FLIP_LEFT:
|
|
GetVectors( NULL, &testDir, NULL );
|
|
testDir.Negate();
|
|
act = NPC_TranslateActivity( (Activity) ACT_ASSASSIN_FLIP_LEFT );
|
|
break;
|
|
|
|
case FLIP_FORWARD:
|
|
GetVectors( &testDir, NULL, NULL );
|
|
act = NPC_TranslateActivity( (Activity) ACT_ASSASSIN_FLIP_FORWARD );
|
|
break;
|
|
|
|
case FLIP_BACKWARD:
|
|
GetVectors( &testDir, NULL, NULL );
|
|
testDir.Negate();
|
|
act = NPC_TranslateActivity( (Activity) ACT_ASSASSIN_FLIP_BACK );
|
|
break;
|
|
|
|
default:
|
|
assert(0); //NOTENOTE: Invalid flip type
|
|
activity = ACT_INVALID;
|
|
return false;
|
|
break;
|
|
}
|
|
|
|
// Make sure we don't flip towards our avoidance position/
|
|
if ( avoidPosition != NULL )
|
|
{
|
|
Vector avoidDir = (*avoidPosition) - GetAbsOrigin();
|
|
VectorNormalize( avoidDir );
|
|
|
|
if ( DotProduct( avoidDir, testDir ) > 0.0f )
|
|
return false;
|
|
}
|
|
|
|
int seq = SelectWeightedSequence( act );
|
|
|
|
// Find out the length of this sequence
|
|
float testDist = GetSequenceMoveDist( seq );
|
|
|
|
// Find the resulting end position from the sequence's movement
|
|
Vector endPos = GetAbsOrigin() + ( testDir * testDist );
|
|
|
|
trace_t tr;
|
|
|
|
if ( ( flipType != FLIP_BACKWARD ) && ( avoidPosition != NULL ) )
|
|
{
|
|
UTIL_TraceLine( (*avoidPosition), endPos, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr );
|
|
|
|
if ( tr.fraction == 1.0f )
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
UTIL_TraceHull( GetAbsOrigin(), endPos, NAI_Hull::Mins(m_eHull) + Vector( 0, 0, StepHeight() ), NAI_Hull::Maxs(m_eHull), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr );
|
|
|
|
// See if we're hit an obstruction in that direction
|
|
if ( tr.fraction < 1.0f )
|
|
{
|
|
if ( g_debug_assassin.GetBool() )
|
|
{
|
|
NDebugOverlay::BoxDirection( GetAbsOrigin(), NAI_Hull::Mins(m_eHull) + Vector( 0, 0, StepHeight() ), NAI_Hull::Maxs(m_eHull) + Vector( testDist, 0, StepHeight() ), testDir, 255, 0, 0, true, 2.0f );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#define NUM_STEPS 2
|
|
|
|
float stepLength = testDist / NUM_STEPS;
|
|
|
|
for ( int i = 1; i <= NUM_STEPS; i++ )
|
|
{
|
|
endPos = GetAbsOrigin() + ( testDir * (stepLength*i) );
|
|
|
|
// Also check for a cliff edge
|
|
UTIL_TraceHull( endPos, endPos - Vector( 0, 0, StepHeight() * 4.0f ), NAI_Hull::Mins(m_eHull) + Vector( 0, 0, StepHeight() ), NAI_Hull::Maxs(m_eHull), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr );
|
|
|
|
if ( tr.fraction == 1.0f )
|
|
{
|
|
if ( g_debug_assassin.GetBool() )
|
|
{
|
|
NDebugOverlay::BoxDirection( endPos, NAI_Hull::Mins(m_eHull) + Vector( 0, 0, StepHeight() ), NAI_Hull::Maxs(m_eHull) + Vector( StepHeight() * 4.0f, 0, StepHeight() ), Vector(0,0,-1), 255, 0, 0, true, 2.0f );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ( g_debug_assassin.GetBool() )
|
|
{
|
|
NDebugOverlay::BoxDirection( GetAbsOrigin(), NAI_Hull::Mins(m_eHull) + Vector( 0, 0, StepHeight() ), NAI_Hull::Maxs(m_eHull) + Vector( testDist, 0, StepHeight() ), testDir, 0, 255, 0, true, 2.0f );
|
|
}
|
|
*/
|
|
|
|
AIMoveTrace_t moveTrace;
|
|
GetMoveProbe()->TestGroundMove( GetAbsOrigin(), endPos, MASK_NPCSOLID, AITGM_DEFAULT, &moveTrace );
|
|
|
|
if ( moveTrace.fStatus != AIMR_OK )
|
|
return false;
|
|
|
|
// Return the activity to use
|
|
activity = (Activity) act;
|
|
|
|
return true;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// Purpose:
|
|
//---------------------------------------------------------
|
|
void CNPC_Assassin::StartTask( const Task_t *pTask )
|
|
{
|
|
switch( pTask->iTask )
|
|
{
|
|
case TASK_ASSASSIN_SET_EYE_STATE:
|
|
{
|
|
SetEyeState( (eyeState_t) ( (int) pTask->flTaskData ) );
|
|
TaskComplete();
|
|
}
|
|
break;
|
|
|
|
case TASK_ASSASSIN_EVADE:
|
|
{
|
|
Activity flipAct = ACT_INVALID;
|
|
|
|
const Vector *avoidPos = ( GetEnemy() != NULL ) ? &(GetEnemy()->GetAbsOrigin()) : NULL;
|
|
|
|
for ( int i = FLIP_LEFT; i < NUM_FLIP_TYPES; i++ )
|
|
{
|
|
if ( CanFlip( i, flipAct, avoidPos ) )
|
|
{
|
|
// Don't flip back to where we just were
|
|
if ( ( ( i == FLIP_LEFT ) && ( m_nLastFlipType == FLIP_RIGHT ) ) ||
|
|
( ( i == FLIP_RIGHT ) && ( m_nLastFlipType == FLIP_LEFT ) ) ||
|
|
( ( i == FLIP_FORWARD ) && ( m_nLastFlipType == FLIP_BACKWARD ) ) ||
|
|
( ( i == FLIP_BACKWARD ) && ( m_nLastFlipType == FLIP_FORWARD ) ) )
|
|
{
|
|
flipAct = ACT_INVALID;
|
|
continue;
|
|
}
|
|
|
|
m_nNumFlips--;
|
|
ResetIdealActivity( flipAct );
|
|
m_flNextFlipTime = gpGlobals->curtime + 2.0f;
|
|
m_nLastFlipType = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( flipAct == ACT_INVALID )
|
|
{
|
|
m_nNumFlips = 0;
|
|
m_nLastFlipType = -1;
|
|
m_flNextFlipTime = gpGlobals->curtime + 2.0f;
|
|
TaskFail( "Unable to find flip evasion direction!\n" );
|
|
}
|
|
}
|
|
break;
|
|
|
|
case TASK_ASSASSIN_GET_PATH_TO_VANTAGE_POINT:
|
|
{
|
|
assert( GetEnemy() != NULL );
|
|
if ( GetEnemy() == NULL )
|
|
break;
|
|
|
|
Vector goalPos;
|
|
|
|
CHintCriteria hint;
|
|
|
|
// Find a disadvantage node near the player, but away from ourselves
|
|
hint.SetHintType( HINT_TACTICAL_ENEMY_DISADVANTAGED );
|
|
hint.AddExcludePosition( GetAbsOrigin(), 256 );
|
|
hint.AddExcludePosition( GetEnemy()->GetAbsOrigin(), 256 );
|
|
|
|
if ( ( m_pSquad != NULL ) && ( m_pSquad->NumMembers() > 1 ) )
|
|
{
|
|
AISquadIter_t iter;
|
|
for ( CAI_BaseNPC *pSquadMember = m_pSquad->GetFirstMember( &iter ); pSquadMember; pSquadMember = m_pSquad->GetNextMember( &iter ) )
|
|
{
|
|
if ( pSquadMember == NULL )
|
|
continue;
|
|
|
|
hint.AddExcludePosition( pSquadMember->GetAbsOrigin(), 128 );
|
|
}
|
|
}
|
|
|
|
hint.SetFlag( bits_HINT_NODE_NEAREST );
|
|
|
|
CAI_Hint *pHint = CAI_HintManager::FindHint( this, GetEnemy()->GetAbsOrigin(), &hint );
|
|
|
|
if ( pHint == NULL )
|
|
{
|
|
TaskFail( "Unable to find vantage point!\n" );
|
|
break;
|
|
}
|
|
|
|
pHint->GetPosition( this, &goalPos );
|
|
|
|
AI_NavGoal_t goal( goalPos );
|
|
|
|
//Try to run directly there
|
|
if ( GetNavigator()->SetGoal( goal ) == false )
|
|
{
|
|
TaskFail( "Unable to find path to vantage point!\n" );
|
|
break;
|
|
}
|
|
|
|
TaskComplete();
|
|
}
|
|
break;
|
|
|
|
default:
|
|
BaseClass::StartTask( pTask );
|
|
break;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
float CNPC_Assassin::MaxYawSpeed( void )
|
|
{
|
|
switch( GetActivity() )
|
|
{
|
|
case ACT_TURN_LEFT:
|
|
case ACT_TURN_RIGHT:
|
|
return 160;
|
|
break;
|
|
case ACT_RUN:
|
|
return 900;
|
|
break;
|
|
case ACT_RANGE_ATTACK1:
|
|
return 0;
|
|
break;
|
|
default:
|
|
return 60;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
void CNPC_Assassin::RunTask( const Task_t *pTask )
|
|
{
|
|
switch( pTask->iTask )
|
|
{
|
|
case TASK_ASSASSIN_EVADE:
|
|
|
|
AutoMovement();
|
|
|
|
if ( IsActivityFinished() )
|
|
{
|
|
TaskComplete();
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
BaseClass::RunTask( pTask );
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
//---------------------------------------------------------
|
|
bool CNPC_Assassin::FValidateHintType ( CAI_Hint *pHint )
|
|
{
|
|
switch( pHint->HintType() )
|
|
{
|
|
case HINT_TACTICAL_ENEMY_DISADVANTAGED:
|
|
{
|
|
Vector hintPos;
|
|
pHint->GetPosition( this, &hintPos );
|
|
|
|
// Verify that we can see the target from that position
|
|
hintPos += GetViewOffset();
|
|
|
|
trace_t tr;
|
|
UTIL_TraceLine( hintPos, GetEnemy()->BodyTarget( hintPos, true ), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr );
|
|
|
|
// Check for seeing our target at the new location
|
|
if ( ( tr.fraction == 1.0f ) || ( tr.m_pEnt == GetEnemy() ) )
|
|
return false;
|
|
|
|
return true;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
return false;
|
|
break;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Output : const Vector
|
|
//-----------------------------------------------------------------------------
|
|
const Vector &CNPC_Assassin::GetViewOffset( void )
|
|
{
|
|
static Vector eyeOffset;
|
|
|
|
//FIXME: Use eye attachment?
|
|
// If we're crouching, offset appropriately
|
|
if ( ( GetActivity() == ACT_ASSASSIN_PERCH ) ||
|
|
( GetActivity() == ACT_RANGE_ATTACK1 ) )
|
|
{
|
|
eyeOffset = Vector( 0, 0, 24.0f );
|
|
}
|
|
else
|
|
{
|
|
eyeOffset = BaseClass::GetViewOffset();
|
|
}
|
|
|
|
return eyeOffset;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CNPC_Assassin::OnScheduleChange( void )
|
|
{
|
|
//TODO: Change eye state?
|
|
|
|
BaseClass::OnScheduleChange();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : state -
|
|
//-----------------------------------------------------------------------------
|
|
void CNPC_Assassin::SetEyeState( eyeState_t state )
|
|
{
|
|
//Must have a valid eye to affect
|
|
if ( ( m_pEyeSprite == NULL ) || ( m_pEyeTrail == NULL ) )
|
|
return;
|
|
|
|
//Set the state
|
|
switch( state )
|
|
{
|
|
default:
|
|
case ASSASSIN_EYE_SEE_TARGET: //Fade in and scale up
|
|
m_pEyeSprite->SetColor( 255, 0, 0 );
|
|
m_pEyeSprite->SetBrightness( 164, 0.1f );
|
|
m_pEyeSprite->SetScale( 0.4f, 0.1f );
|
|
|
|
m_pEyeTrail->SetColor( 255, 0, 0 );
|
|
m_pEyeTrail->SetScale( 8.0f );
|
|
m_pEyeTrail->SetBrightness( 164 );
|
|
|
|
break;
|
|
|
|
case ASSASSIN_EYE_SEEKING_TARGET: //Ping-pongs
|
|
|
|
//Toggle our state
|
|
m_bBlinkState = !m_bBlinkState;
|
|
m_pEyeSprite->SetColor( 255, 128, 0 );
|
|
|
|
if ( m_bBlinkState )
|
|
{
|
|
//Fade up and scale up
|
|
m_pEyeSprite->SetScale( 0.25f, 0.1f );
|
|
m_pEyeSprite->SetBrightness( 164, 0.1f );
|
|
}
|
|
else
|
|
{
|
|
//Fade down and scale down
|
|
m_pEyeSprite->SetScale( 0.2f, 0.1f );
|
|
m_pEyeSprite->SetBrightness( 64, 0.1f );
|
|
}
|
|
|
|
break;
|
|
|
|
case ASSASSIN_EYE_DORMANT: //Fade out and scale down
|
|
m_pEyeSprite->SetScale( 0.5f, 0.5f );
|
|
m_pEyeSprite->SetBrightness( 64, 0.5f );
|
|
|
|
m_pEyeTrail->SetScale( 2.0f );
|
|
m_pEyeTrail->SetBrightness( 64 );
|
|
break;
|
|
|
|
case ASSASSIN_EYE_DEAD: //Fade out slowly
|
|
m_pEyeSprite->SetColor( 255, 0, 0 );
|
|
m_pEyeSprite->SetScale( 0.1f, 5.0f );
|
|
m_pEyeSprite->SetBrightness( 0, 5.0f );
|
|
|
|
m_pEyeTrail->SetColor( 255, 0, 0 );
|
|
m_pEyeTrail->SetScale( 0.1f );
|
|
m_pEyeTrail->SetBrightness( 0 );
|
|
break;
|
|
|
|
case ASSASSIN_EYE_ACTIVE:
|
|
m_pEyeSprite->SetColor( 255, 0, 0 );
|
|
m_pEyeSprite->SetScale( 0.1f );
|
|
m_pEyeSprite->SetBrightness( 0 );
|
|
break;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CNPC_Assassin::GatherEnemyConditions( CBaseEntity *pEnemy )
|
|
{
|
|
ClearCondition( COND_ASSASSIN_ENEMY_TARGETTING_ME );
|
|
|
|
BaseClass::GatherEnemyConditions( pEnemy );
|
|
|
|
// See if we're being targetted specifically
|
|
if ( HasCondition( COND_ENEMY_FACING_ME ) )
|
|
{
|
|
Vector enemyDir = GetAbsOrigin() - pEnemy->GetAbsOrigin();
|
|
VectorNormalize( enemyDir );
|
|
|
|
Vector enemyBodyDir;
|
|
CBasePlayer *pPlayer = ToBasePlayer( pEnemy );
|
|
|
|
if ( pPlayer != NULL )
|
|
{
|
|
enemyBodyDir = pPlayer->BodyDirection3D();
|
|
}
|
|
else
|
|
{
|
|
AngleVectors( pEnemy->GetAbsAngles(), &enemyBodyDir );
|
|
}
|
|
|
|
float enemyDot = DotProduct( enemyBodyDir, enemyDir );
|
|
|
|
//FIXME: Need to refine this a bit
|
|
if ( enemyDot > 0.97f )
|
|
{
|
|
SetCondition( COND_ASSASSIN_ENEMY_TARGETTING_ME );
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CNPC_Assassin::BuildScheduleTestBits( void )
|
|
{
|
|
SetNextThink( gpGlobals->curtime + 0.05 );
|
|
|
|
//Don't allow any modifications when scripted
|
|
if ( m_NPCState == NPC_STATE_SCRIPT )
|
|
return;
|
|
|
|
//Become interrupted if we're targetted when shooting an enemy
|
|
if ( IsCurSchedule( SCHED_RANGE_ATTACK1 ) )
|
|
{
|
|
SetCustomInterruptCondition( COND_ASSASSIN_ENEMY_TARGETTING_ME );
|
|
}
|
|
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : &info -
|
|
//-----------------------------------------------------------------------------
|
|
void CNPC_Assassin::Event_Killed( const CTakeDamageInfo &info )
|
|
{
|
|
BaseClass::Event_Killed( info );
|
|
|
|
// Turn off the eye
|
|
SetEyeState( ASSASSIN_EYE_DEAD );
|
|
|
|
// Turn off the pistols
|
|
SetBodygroup( 1, 0 );
|
|
|
|
// Spawn her guns
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//
|
|
// Schedules
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
|
|
AI_BEGIN_CUSTOM_NPC( npc_assassin, CNPC_Assassin )
|
|
|
|
DECLARE_ACTIVITY(ACT_ASSASSIN_FLIP_LEFT)
|
|
DECLARE_ACTIVITY(ACT_ASSASSIN_FLIP_RIGHT)
|
|
DECLARE_ACTIVITY(ACT_ASSASSIN_FLIP_BACK)
|
|
DECLARE_ACTIVITY(ACT_ASSASSIN_FLIP_FORWARD)
|
|
DECLARE_ACTIVITY(ACT_ASSASSIN_PERCH)
|
|
|
|
//Adrian: events go here
|
|
DECLARE_ANIMEVENT( AE_ASSASIN_FIRE_PISTOL_RIGHT )
|
|
DECLARE_ANIMEVENT( AE_ASSASIN_FIRE_PISTOL_LEFT )
|
|
DECLARE_ANIMEVENT( AE_ASSASIN_KICK_HIT )
|
|
|
|
DECLARE_TASK(TASK_ASSASSIN_GET_PATH_TO_VANTAGE_POINT)
|
|
DECLARE_TASK(TASK_ASSASSIN_EVADE)
|
|
DECLARE_TASK(TASK_ASSASSIN_SET_EYE_STATE)
|
|
DECLARE_TASK(TASK_ASSASSIN_LUNGE)
|
|
|
|
DECLARE_CONDITION(COND_ASSASSIN_ENEMY_TARGETTING_ME)
|
|
|
|
//=========================================================
|
|
// ASSASSIN_STALK_ENEMY
|
|
//=========================================================
|
|
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_ASSASSIN_STALK_ENEMY,
|
|
|
|
" Tasks"
|
|
" TASK_STOP_MOVING 0"
|
|
" TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_ASSASSIN_PERCH"
|
|
" "
|
|
" Interrupts"
|
|
" COND_ASSASSIN_ENEMY_TARGETTING_ME"
|
|
" COND_SEE_ENEMY"
|
|
" COND_LIGHT_DAMAGE"
|
|
" COND_HEAVY_DAMAGE"
|
|
)
|
|
|
|
//=========================================================
|
|
// > ASSASSIN_FIND_VANTAGE_POINT
|
|
//=========================================================
|
|
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_ASSASSIN_FIND_VANTAGE_POINT,
|
|
|
|
" Tasks"
|
|
" TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_TAKE_COVER_FROM_ENEMY"
|
|
" TASK_STOP_MOVING 0"
|
|
" TASK_ASSASSIN_GET_PATH_TO_VANTAGE_POINT 0"
|
|
" TASK_RUN_PATH 0"
|
|
" TASK_WAIT_FOR_MOVEMENT 0"
|
|
" TASK_SET_SCHEDULE SCHEDULE:SCHED_ASSASSIN_STALK_ENEMY"
|
|
" "
|
|
" Interrupts"
|
|
" COND_LIGHT_DAMAGE"
|
|
" COND_HEAVY_DAMAGE"
|
|
" COND_TASK_FAILED"
|
|
)
|
|
|
|
//=========================================================
|
|
// Assassin needs to avoid the player
|
|
//=========================================================
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_ASSASSIN_EVADE,
|
|
|
|
" Tasks"
|
|
" TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ASSASSIN_FIND_VANTAGE_POINT"
|
|
" TASK_STOP_MOVING 0"
|
|
" TASK_ASSASSIN_EVADE 0"
|
|
" "
|
|
" Interrupts"
|
|
" COND_TASK_FAILED"
|
|
)
|
|
|
|
//=========================================================
|
|
// Assassin needs to avoid the player
|
|
//=========================================================
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_ASSASSIN_LUNGE,
|
|
|
|
" Tasks"
|
|
" TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ASSASSIN_FIND_VANTAGE_POINT"
|
|
" TASK_STOP_MOVING 0"
|
|
" TASK_FACE_ENEMY 0"
|
|
" TASK_PLAY_SEQUENCE ACTIVITY:ACT_ASSASSIN_FLIP_FORWARD"
|
|
" "
|
|
" Interrupts"
|
|
" COND_TASK_FAILED"
|
|
)
|
|
|
|
AI_END_CUSTOM_NPC()
|