//========= 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()