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