|
|
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: Father Grigori, a benevolent monk who is the last remaining human
// in Ravenholm. He keeps to the rooftops and uses a big ole elephant
// gun to send his zombified former friends to a peaceful death.
//
//=============================================================================//
#include "cbase.h"
#include "ai_baseactor.h"
#include "ai_hull.h"
#include "ammodef.h"
#include "gamerules.h"
#include "IEffects.h"
#include "engine/IEngineSound.h"
#include "ai_behavior.h"
#include "ai_behavior_assault.h"
#include "ai_behavior_lead.h"
#include "npcevent.h"
#include "ai_playerally.h"
#include "ai_senses.h"
#include "soundent.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
ConVar monk_headshot_freq( "monk_headshot_freq", "2" );
//-----------------------------------------------------------------------------
// Activities.
//-----------------------------------------------------------------------------
int ACT_MONK_GUN_IDLE;
class CNPC_Monk : public CAI_PlayerAlly { DECLARE_CLASS( CNPC_Monk, CAI_PlayerAlly );
public:
CNPC_Monk() {} void Spawn(); void Precache();
bool CreateBehaviors(); int GetSoundInterests(); void BuildScheduleTestBits( void ); Class_T Classify( void );
bool ShouldBackAway();
bool IsValidEnemy( CBaseEntity *pEnemy );
int TranslateSchedule( int scheduleType ); int SelectSchedule ();
void HandleAnimEvent( animevent_t *pEvent ); Activity NPC_TranslateActivity( Activity eNewActivity );
void PainSound( const CTakeDamageInfo &info ); void DeathSound( const CTakeDamageInfo &info ); WeaponProficiency_t CalcWeaponProficiency( CBaseCombatWeapon *pWeapon ); Vector GetActualShootPosition( const Vector &shootOrigin ); Vector GetActualShootTrajectory( const Vector &shootOrigin );
void PrescheduleThink();
void StartTask( const Task_t *pTask ); void RunTask( const Task_t *pTask );
void GatherConditions();
bool PassesDamageFilter( const CTakeDamageInfo &info ); void OnKilledNPC( CBaseCombatCharacter *pKilled );
bool IsJumpLegal( const Vector &startPos, const Vector &apex, const Vector &endPos ) const; int SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode );
DECLARE_DATADESC();
private: //-----------------------------------------------------
// Conditions, Schedules, Tasks
//-----------------------------------------------------
enum { SCHED_MONK_RANGE_ATTACK1 = BaseClass::NEXT_SCHEDULE, SCHED_MONK_BACK_AWAY_FROM_ENEMY, SCHED_MONK_BACK_AWAY_AND_RELOAD, SCHED_MONK_NORMAL_RELOAD, };
/*enum
{ //TASK_MONK_FIRST_TASK = BaseClass::NEXT_TASK,
};*/
DEFINE_CUSTOM_AI;
// Inputs
void InputPerfectAccuracyOn( inputdata_t &inputdata ); void InputPerfectAccuracyOff( inputdata_t &inputdata ); CAI_AssaultBehavior m_AssaultBehavior; CAI_LeadBehavior m_LeadBehavior; int m_iNumZombies; int m_iDangerousZombies; bool m_bPerfectAccuracy; bool m_bMournedPlayer;
};
BEGIN_DATADESC( CNPC_Monk ) // m_AssaultBehavior
// m_LeadBehavior
DEFINE_FIELD( m_iNumZombies, FIELD_INTEGER ), DEFINE_FIELD( m_iDangerousZombies, FIELD_INTEGER ), DEFINE_FIELD( m_bPerfectAccuracy, FIELD_BOOLEAN ), DEFINE_FIELD( m_bMournedPlayer, FIELD_BOOLEAN ),
// Inputs
DEFINE_INPUTFUNC( FIELD_VOID, "PerfectAccuracyOn", InputPerfectAccuracyOn ), DEFINE_INPUTFUNC( FIELD_VOID, "PerfectAccuracyOff", InputPerfectAccuracyOff ),
END_DATADESC()
LINK_ENTITY_TO_CLASS( npc_monk, CNPC_Monk );
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
bool CNPC_Monk::CreateBehaviors() { AddBehavior( &m_LeadBehavior ); AddBehavior( &m_AssaultBehavior ); return BaseClass::CreateBehaviors(); }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
int CNPC_Monk::GetSoundInterests() { return SOUND_WORLD | SOUND_COMBAT | SOUND_PLAYER | SOUND_DANGER; }
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_Monk::BuildScheduleTestBits( void ) { // FIXME: we need a way to make scenes non-interruptible
#if 0
if ( IsCurSchedule( SCHED_RANGE_ATTACK1 ) || IsCurSchedule( SCHED_SCENE_GENERIC ) ) { ClearCustomInterruptCondition( COND_LIGHT_DAMAGE ); ClearCustomInterruptCondition( COND_HEAVY_DAMAGE ); ClearCustomInterruptCondition( COND_NEW_ENEMY ); ClearCustomInterruptCondition( COND_HEAR_DANGER ); } #endif
// Don't interrupt while shooting the gun
const Task_t* pTask = GetTask(); if ( pTask && (pTask->iTask == TASK_RANGE_ATTACK1) ) { ClearCustomInterruptCondition( COND_HEAVY_DAMAGE ); ClearCustomInterruptCondition( COND_ENEMY_OCCLUDED ); ClearCustomInterruptCondition( COND_HEAR_DANGER ); ClearCustomInterruptCondition( COND_WEAPON_BLOCKED_BY_FRIEND ); ClearCustomInterruptCondition( COND_WEAPON_SIGHT_OCCLUDED ); } }
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
Class_T CNPC_Monk::Classify( void ) { return CLASS_PLAYER_ALLY_VITAL; }
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
Activity CNPC_Monk::NPC_TranslateActivity( Activity eNewActivity ) { eNewActivity = BaseClass::NPC_TranslateActivity( eNewActivity );
if ( (m_NPCState == NPC_STATE_COMBAT || m_NPCState == NPC_STATE_ALERT) ) { bool bGunUp = false;
bGunUp = (gpGlobals->curtime - m_flLastAttackTime < 4); bGunUp = bGunUp || (GetEnemy() && !HasCondition( COND_TOO_FAR_TO_ATTACK ));
if (bGunUp) { if ( eNewActivity == ACT_IDLE ) { eNewActivity = ACT_IDLE_ANGRY; } // keep aiming a little longer than normal since the shot takes so long and there's no good way to do a transitions between movement types :/
else if ( eNewActivity == ACT_WALK ) { eNewActivity = ACT_WALK_AIM; } else if ( eNewActivity == ACT_RUN ) { eNewActivity = ACT_RUN_AIM; } } }
// We need these so that we can pick up the shotgun to throw it in the balcony scene
if ( eNewActivity == ACT_IDLE_ANGRY_SHOTGUN ) { eNewActivity = ACT_IDLE_ANGRY_SMG1; } else if ( eNewActivity == ACT_WALK_AIM_SHOTGUN ) { eNewActivity = ACT_WALK_AIM_RIFLE; } else if ( eNewActivity == ACT_RUN_AIM_SHOTGUN ) { eNewActivity = ACT_RUN_AIM_RIFLE; } else if ( eNewActivity == ACT_RANGE_ATTACK_SHOTGUN_LOW ) { return ACT_RANGE_ATTACK_SMG1_LOW; }
return eNewActivity; }
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_Monk::Precache() { PrecacheModel( "models/Monk.mdl" ); PrecacheScriptSound( "NPC_Citizen.FootstepLeft" ); PrecacheScriptSound( "NPC_Citizen.FootstepRight" );
BaseClass::Precache(); }
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_Monk::Spawn() { Precache();
BaseClass::Spawn();
SetModel( "models/Monk.mdl" );
SetHullType(HULL_HUMAN); SetHullSizeNormal();
SetSolid( SOLID_BBOX ); AddSolidFlags( FSOLID_NOT_STANDABLE ); SetMoveType( MOVETYPE_STEP ); SetBloodColor( BLOOD_COLOR_RED ); m_iHealth = 100; m_flFieldOfView = m_flFieldOfView = -0.707; // 270`
m_NPCState = NPC_STATE_NONE;
m_HackedGunPos = Vector ( 0, 0, 55 );
CapabilitiesAdd( bits_CAP_TURN_HEAD | bits_CAP_DOORS_GROUP | bits_CAP_MOVE_GROUND ); CapabilitiesAdd( bits_CAP_USE_WEAPONS ); CapabilitiesAdd( bits_CAP_ANIMATEDFACE ); CapabilitiesAdd( bits_CAP_FRIENDLY_DMG_IMMUNE ); CapabilitiesAdd( bits_CAP_AIM_GUN ); CapabilitiesAdd( bits_CAP_MOVE_SHOOT );
NPCInit(); }
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
void CNPC_Monk::PainSound( const CTakeDamageInfo &info ) { SpeakIfAllowed( TLK_WOUND ); }
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
void CNPC_Monk::DeathSound( const CTakeDamageInfo &info ) { // Sentences don't play on dead NPCs
SentenceStop();
Speak( TLK_DEATH ); }
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
WeaponProficiency_t CNPC_Monk::CalcWeaponProficiency( CBaseCombatWeapon *pWeapon ) { return WEAPON_PROFICIENCY_PERFECT; }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
Vector CNPC_Monk::GetActualShootPosition( const Vector &shootOrigin ) { return BaseClass::GetActualShootPosition( shootOrigin ); }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
Vector CNPC_Monk::GetActualShootTrajectory( const Vector &shootOrigin ) { if( GetEnemy() && GetEnemy()->Classify() == CLASS_ZOMBIE ) { Vector vecShootDir;
if( m_bPerfectAccuracy || random->RandomInt( 1, monk_headshot_freq.GetInt() ) == 1 ) { vecShootDir = GetEnemy()->HeadTarget( shootOrigin ) - shootOrigin; } else { vecShootDir = GetEnemy()->BodyTarget( shootOrigin ) - shootOrigin; }
VectorNormalize( vecShootDir ); return vecShootDir; }
return BaseClass::GetActualShootTrajectory( shootOrigin ); }
//-----------------------------------------------------------------------------
// Purpose:
// Input : pEvent -
//-----------------------------------------------------------------------------
void CNPC_Monk::HandleAnimEvent( animevent_t *pEvent ) { switch( pEvent->event ) { case NPC_EVENT_LEFTFOOT: { EmitSound( "NPC_Citizen.FootstepLeft", pEvent->eventtime ); } break; case NPC_EVENT_RIGHTFOOT: { EmitSound( "NPC_Citizen.FootstepRight", pEvent->eventtime ); } break;
default: BaseClass::HandleAnimEvent( pEvent ); break; } }
//-------------------------------------
// Grigori tries to stand his ground until
// enemies are very close.
//-------------------------------------
#define MONK_STAND_GROUND_HEIGHT 24.0
bool CNPC_Monk::ShouldBackAway() { if( !GetEnemy() ) return false;
if( GetAbsOrigin().z - GetEnemy()->GetAbsOrigin().z >= MONK_STAND_GROUND_HEIGHT ) { // This is a fairly special case. Grigori looks better fighting from his assault points in the
// elevated places of the Graveyard, so we prevent his back away behavior anytime he has a height
// advantage on his enemy.
return false; }
float flDist; flDist = ( GetAbsOrigin() - GetEnemy()->GetAbsOrigin() ).Length();
if( flDist <= 180 ) return true;
return false; }
//-------------------------------------
bool CNPC_Monk::IsValidEnemy( CBaseEntity *pEnemy ) { if ( BaseClass::IsValidEnemy( pEnemy ) && GetActiveWeapon() ) { float flDist;
flDist = ( GetAbsOrigin() - pEnemy->GetAbsOrigin() ).Length(); if( flDist <= GetActiveWeapon()->m_fMaxRange1 ) return true; } return false; }
//-------------------------------------
int CNPC_Monk::TranslateSchedule( int scheduleType ) { switch( scheduleType ) { case SCHED_MOVE_AWAY_FAIL: // Our first method of backing away failed. Try another.
return SCHED_MONK_BACK_AWAY_FROM_ENEMY; break;
case SCHED_RANGE_ATTACK1: { if( ShouldBackAway() ) { // Get some room, rely on move and shoot.
return SCHED_MOVE_AWAY; }
return SCHED_MONK_RANGE_ATTACK1; } break;
case SCHED_HIDE_AND_RELOAD: case SCHED_RELOAD: if( ShouldBackAway() ) { return SCHED_MONK_BACK_AWAY_AND_RELOAD; }
return SCHED_RELOAD; break; }
return BaseClass::TranslateSchedule( scheduleType ); }
//-------------------------------------
void CNPC_Monk::PrescheduleThink() { BaseClass::PrescheduleThink(); }
//-------------------------------------
int CNPC_Monk::SelectSchedule() { if( HasCondition( COND_HEAR_DANGER ) ) { SpeakIfAllowed( TLK_DANGER ); return SCHED_TAKE_COVER_FROM_BEST_SOUND; }
if ( HasCondition( COND_TALKER_PLAYER_DEAD ) && !m_bMournedPlayer && IsOkToSpeak() ) { m_bMournedPlayer = true; Speak( TLK_IDLE ); }
if( !BehaviorSelectSchedule() ) { if ( HasCondition ( COND_NO_PRIMARY_AMMO ) ) { return SCHED_HIDE_AND_RELOAD; } }
return BaseClass::SelectSchedule(); }
//-------------------------------------
void CNPC_Monk::StartTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_RELOAD: { if ( GetActiveWeapon() && GetActiveWeapon()->HasPrimaryAmmo() ) { // Don't reload if you have done so while moving (See BACK_AWAY_AND_RELOAD schedule).
TaskComplete(); return; }
if( m_iNumZombies >= 2 && random->RandomInt( 1, 3 ) == 1 ) { SpeakIfAllowed( TLK_ATTACKING ); }
Activity reloadGesture = TranslateActivity( ACT_GESTURE_RELOAD ); if ( reloadGesture != ACT_INVALID && IsPlayingGesture( reloadGesture ) ) { ResetIdealActivity( ACT_IDLE ); return; }
BaseClass::StartTask( pTask ); } break;
default: BaseClass::StartTask( pTask ); break; } }
void CNPC_Monk::RunTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_RELOAD: { Activity reloadGesture = TranslateActivity( ACT_GESTURE_RELOAD ); if ( GetIdealActivity() != ACT_RELOAD && reloadGesture != ACT_INVALID ) { if ( !IsPlayingGesture( reloadGesture ) ) { if ( GetShotRegulator() ) { GetShotRegulator()->Reset( false ); }
TaskComplete(); } return; }
BaseClass::RunTask( pTask ); } break;
default: BaseClass::RunTask( pTask ); break; } }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_Monk::GatherConditions() { BaseClass::GatherConditions();
// Build my zombie danger index!
m_iNumZombies = 0; m_iDangerousZombies = 0;
AISightIter_t iter; CBaseEntity *pSightEnt; pSightEnt = GetSenses()->GetFirstSeenEntity( &iter ); while( pSightEnt ) { if( pSightEnt->Classify() == CLASS_ZOMBIE && pSightEnt->IsAlive() ) { // Is this zombie coming for me?
CAI_BaseNPC *pZombie = dynamic_cast<CAI_BaseNPC*>(pSightEnt); if( pZombie && pZombie->GetEnemy() == this ) { m_iNumZombies++;
// if this zombie is close enough to attack, add him to the zombie danger!
float flDist;
flDist = (pZombie->GetAbsOrigin() - GetAbsOrigin()).Length2DSqr();
if( flDist <= 128.0f * 128.0f ) { m_iDangerousZombies++; } } }
pSightEnt = GetSenses()->GetNextSeenEntity( &iter ); }
if( m_iDangerousZombies >= 3 || (GetEnemy() && GetHealth() < 25) ) { // I see many zombies, or I'm quite injured.
SpeakIfAllowed( TLK_HELP_ME ); }
// NOTE!!!!!! This code assumes grigori is using annabelle!
ClearCondition(COND_LOW_PRIMARY_AMMO); if ( GetActiveWeapon() ) { if ( GetActiveWeapon()->UsesPrimaryAmmo() ) { if (!GetActiveWeapon()->HasPrimaryAmmo() ) { SetCondition(COND_NO_PRIMARY_AMMO); } else if ( m_NPCState != NPC_STATE_COMBAT && GetActiveWeapon()->UsesClipsForAmmo1() && GetActiveWeapon()->Clip1() < 2 ) { // Don't send a low ammo message unless we're not in combat.
SetCondition(COND_LOW_PRIMARY_AMMO); } } } }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
bool CNPC_Monk::PassesDamageFilter( const CTakeDamageInfo &info ) { if ( info.GetAttacker()->ClassMatches( "npc_headcrab_black" ) || info.GetAttacker()->ClassMatches( "npc_headcrab_poison" ) ) return false;
return BaseClass::PassesDamageFilter( info ); }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_Monk::OnKilledNPC( CBaseCombatCharacter *pKilled ) { if ( !pKilled ) { return; }
if ( pKilled->Classify() == CLASS_ZOMBIE ) { // Don't speak if the gun is empty, cause grigori will want to speak while he's reloading.
if ( GetActiveWeapon() ) { if ( GetActiveWeapon()->UsesPrimaryAmmo() && !GetActiveWeapon()->HasPrimaryAmmo() ) { // Gun is empty. I'm about to reload.
if( m_iNumZombies >= 2 ) { // Don't talk about killing a single zombie if there are more coming.
// the reload behavior will say "come to me, children", etc.
return; } } }
if( m_iNumZombies == 1 || random->RandomInt( 1, 3 ) == 1 ) { SpeakIfAllowed( TLK_ENEMY_DEAD ); } } }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
int CNPC_Monk::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode ) { if( failedSchedule == SCHED_MONK_BACK_AWAY_FROM_ENEMY ) { if( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) { // Most likely backed into a corner. Just blaze away.
return SCHED_MONK_RANGE_ATTACK1; } }
return BaseClass::SelectFailSchedule( failedSchedule, failedTask, taskFailCode ); }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
bool CNPC_Monk::IsJumpLegal( const Vector &startPos, const Vector &apex, const Vector &endPos ) const { if ( startPos.z - endPos.z < 0 ) return false; return BaseClass::IsJumpLegal( startPos, apex, endPos ); }
//-----------------------------------------------------------------------------
// Every shot's a headshot. Useful for scripted Grigoris
//-----------------------------------------------------------------------------
void CNPC_Monk::InputPerfectAccuracyOn( inputdata_t &inputdata ) { m_bPerfectAccuracy = true; }
//-----------------------------------------------------------------------------
// Turn off perfect accuracy.
//-----------------------------------------------------------------------------
void CNPC_Monk::InputPerfectAccuracyOff( inputdata_t &inputdata ) { m_bPerfectAccuracy = false; }
//-----------------------------------------------------------------------------
//
// CNPC_Monk Schedules
//
//-----------------------------------------------------------------------------
AI_BEGIN_CUSTOM_NPC( npc_monk, CNPC_Monk )
DECLARE_ACTIVITY( ACT_MONK_GUN_IDLE )
DEFINE_SCHEDULE ( SCHED_MONK_RANGE_ATTACK1,
" Tasks" " TASK_STOP_MOVING 0" " TASK_FACE_ENEMY 0" " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
" TASK_RANGE_ATTACK1 0" "" " Interrupts" " COND_HEAVY_DAMAGE" " COND_ENEMY_OCCLUDED" " COND_HEAR_DANGER" " COND_WEAPON_BLOCKED_BY_FRIEND" " COND_WEAPON_SIGHT_OCCLUDED" )
DEFINE_SCHEDULE ( SCHED_MONK_BACK_AWAY_FROM_ENEMY,
" Tasks" " TASK_STOP_MOVING 0" " TASK_STORE_ENEMY_POSITION_IN_SAVEPOSITION 0" " TASK_FIND_BACKAWAY_FROM_SAVEPOSITION 0" " TASK_WALK_PATH_TIMED 4.0" " TASK_WAIT_FOR_MOVEMENT 0" "" " Interrupts" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" );
DEFINE_SCHEDULE ( SCHED_MONK_BACK_AWAY_AND_RELOAD,
" Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_MONK_NORMAL_RELOAD" " TASK_STOP_MOVING 0" " TASK_STORE_ENEMY_POSITION_IN_SAVEPOSITION 0" " TASK_FIND_BACKAWAY_FROM_SAVEPOSITION 0" " TASK_WALK_PATH_TIMED 2.0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_STOP_MOVING 0" " TASK_RELOAD 0" "" " Interrupts" " COND_ENEMY_DEAD" );
DEFINE_SCHEDULE ( SCHED_MONK_NORMAL_RELOAD,
" Tasks" " TASK_STOP_MOVING 0" " TASK_RELOAD 0" "" " Interrupts" " COND_HEAR_DANGER" );
AI_END_CUSTOM_NPC()
|