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.
1101 lines
28 KiB
1101 lines
28 KiB
//========= 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()
|
|
|