diff options
Diffstat (limited to 'mp/src/game/server/hl2/npc_metropolice.cpp')
| -rw-r--r-- | mp/src/game/server/hl2/npc_metropolice.cpp | 5846 |
1 files changed, 5846 insertions, 0 deletions
diff --git a/mp/src/game/server/hl2/npc_metropolice.cpp b/mp/src/game/server/hl2/npc_metropolice.cpp new file mode 100644 index 00000000..f87b473b --- /dev/null +++ b/mp/src/game/server/hl2/npc_metropolice.cpp @@ -0,0 +1,5846 @@ +//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+//=============================================================================//
+
+#include "cbase.h"
+#include "soundent.h"
+#include "npcevent.h"
+#include "globalstate.h"
+#include "ai_squad.h"
+#include "ai_tacticalservices.h"
+#include "npc_manhack.h"
+#include "npc_metropolice.h"
+#include "weapon_stunstick.h"
+#include "basegrenade_shared.h"
+#include "ai_route.h"
+#include "hl2_player.h"
+#include "iservervehicle.h"
+#include "items.h"
+#include "hl2_gamerules.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+//#define SF_METROPOLICE_ 0x00010000
+#define SF_METROPOLICE_SIMPLE_VERSION 0x00020000
+#define SF_METROPOLICE_ALWAYS_STITCH 0x00080000
+#define SF_METROPOLICE_NOCHATTER 0x00100000
+#define SF_METROPOLICE_ARREST_ENEMY 0x00200000
+#define SF_METROPOLICE_NO_FAR_STITCH 0x00400000
+#define SF_METROPOLICE_NO_MANHACK_DEPLOY 0x00800000
+#define SF_METROPOLICE_ALLOWED_TO_RESPOND 0x01000000
+#define SF_METROPOLICE_MID_RANGE_ATTACK 0x02000000
+
+#define METROPOLICE_MID_RANGE_ATTACK_RANGE 3500.0f
+
+#define METROPOLICE_SQUAD_STITCH_MIN_INTERVAL 1.0f
+#define METROPOLICE_SQUAD_STITCH_MAX_INTERVAL 1.2f
+
+#define AIM_ALONG_SIDE_LINE_OF_DEATH_DISTANCE 300.0f
+#define AIM_ALONG_SIDE_STEER_DISTANCE 200.0f
+#define AIM_ALONG_SIDE_DEFAULT_STITCH_LENGTH 750.0f
+#define AIM_ALONG_SIDE_LINE_OF_DEATH_LEAD_TIME 0.0f
+#define AIM_ALONG_SIDE_LINE_INITIAL_DRAW_FRACTION 0.2f
+
+#define AIM_BEHIND_DEFAULT_STITCH_LENGTH 1000.0f
+#define AIM_BEHIND_MINIMUM_DISTANCE 650.0f
+#define AIM_BEHIND_STEER_DISTANCE 150.0f
+
+#define RECENT_DAMAGE_INTERVAL 3.0f
+#define RECENT_DAMAGE_THRESHOLD 0.2f
+
+#define VEHICLE_PREDICT_ACCELERATION 333.0f
+#define VEHICLE_PREDICT_MAX_SPEED 600.0f
+
+#define METROPOLICE_MAX_WARNINGS 3
+
+#define METROPOLICE_BODYGROUP_MANHACK 1
+
+enum
+{
+ // NOTE: Exact #s are important, since they are referred to by number in schedules below
+
+ METROPOLICE_SENTENCE_FREEZE = 0,
+ METROPOLICE_SENTENCE_HES_OVER_HERE = 1,
+ METROPOLICE_SENTENCE_HES_RUNNING = 2,
+ METROPOLICE_SENTENCE_TAKE_HIM_DOWN = 3,
+ METROPOLICE_SENTENCE_ARREST_IN_POSITION = 4,
+ METROPOLICE_SENTENCE_DEPLOY_MANHACK = 5,
+ METROPOLICE_SENTENCE_MOVE_INTO_POSITION = 6,
+ METROPOLICE_SENTENCE_HEARD_SOMETHING = 7,
+};
+
+enum
+{
+ METROPOLICE_ANNOUNCE_ATTACK_PRIMARY = 1,
+ METROPOLICE_ANNOUNCE_ATTACK_SECONDARY,
+ METROPOLICE_ANNOUNCE_ATTACK_HARASS,
+};
+
+enum
+{
+ METROPOLICE_CHATTER_WAIT_FOR_RESPONSE = 0,
+ METROPOLICE_CHATTER_ASK_QUESTION = 1,
+ METROPOLICE_CHATTER_RESPONSE = 2,
+
+ METROPOLICE_CHATTER_RESPONSE_TYPE_COUNT = 2,
+};
+
+
+enum SpeechMemory_t
+{
+ bits_MEMORY_PAIN_LIGHT_SOUND = bits_MEMORY_CUSTOM1,
+ bits_MEMORY_PAIN_HEAVY_SOUND = bits_MEMORY_CUSTOM2,
+ bits_MEMORY_PLAYER_HURT = bits_MEMORY_CUSTOM3,
+ bits_MEMORY_PLAYER_HARASSED = bits_MEMORY_CUSTOM4,
+};
+
+//Metrocop
+int g_interactionMetrocopStartedStitch = 0;
+int g_interactionMetrocopIdleChatter = 0;
+int g_interactionMetrocopClearSentenceQueues = 0;
+
+extern int g_interactionHitByPlayerThrownPhysObj;
+
+ConVar sk_metropolice_stitch_reaction( "sk_metropolice_stitch_reaction","1.0");
+ConVar sk_metropolice_stitch_tight_hitcount( "sk_metropolice_stitch_tight_hitcount","2");
+ConVar sk_metropolice_stitch_at_hitcount( "sk_metropolice_stitch_at_hitcount","1");
+ConVar sk_metropolice_stitch_behind_hitcount( "sk_metropolice_stitch_behind_hitcount","3");
+ConVar sk_metropolice_stitch_along_hitcount( "sk_metropolice_stitch_along_hitcount","2");
+
+
+ConVar sk_metropolice_health( "sk_metropolice_health","0");
+ConVar sk_metropolice_simple_health( "sk_metropolice_simple_health","26");
+ConVar sk_metropolice_stitch_distance( "sk_metropolice_stitch_distance","1000");
+
+ConVar metropolice_chase_use_follow( "metropolice_chase_use_follow", "0" );
+ConVar metropolice_move_and_melee("metropolice_move_and_melee", "1" );
+ConVar metropolice_charge("metropolice_charge", "1" );
+
+// How many clips of pistol ammo a metropolice carries.
+#define METROPOLICE_NUM_CLIPS 5
+#define METROPOLICE_BURST_RELOAD_COUNT 20
+
+int AE_METROPOLICE_BATON_ON;
+int AE_METROPOLICE_BATON_OFF;
+int AE_METROPOLICE_SHOVE;
+int AE_METROPOLICE_START_DEPLOY;
+int AE_METROPOLICE_DRAW_PISTOL; // was 50
+int AE_METROPOLICE_DEPLOY_MANHACK; // was 51
+
+// -----------------------------------------------
+// > Squad slots
+// -----------------------------------------------
+enum SquadSlot_T
+{
+ SQUAD_SLOT_POLICE_CHARGE_ENEMY = LAST_SHARED_SQUADSLOT,
+ SQUAD_SLOT_POLICE_HARASS, // Yell at the player with a megaphone, etc.
+ SQUAD_SLOT_POLICE_DEPLOY_MANHACK,
+ SQUAD_SLOT_POLICE_ADVANCE,
+ SQUAD_SLOT_POLICE_ATTACK_OCCLUDER1,
+ SQUAD_SLOT_POLICE_ATTACK_OCCLUDER2,
+ SQUAD_SLOT_POLICE_COVERING_FIRE1,
+ SQUAD_SLOT_POLICE_COVERING_FIRE2,
+ SQUAD_SLOT_POLICE_ARREST_ENEMY,
+};
+
+//=========================================================
+// Metro Police Activities
+//=========================================================
+int ACT_METROPOLICE_DRAW_PISTOL;
+int ACT_METROPOLICE_DEPLOY_MANHACK;
+int ACT_METROPOLICE_FLINCH_BEHIND;
+
+int ACT_WALK_BATON;
+int ACT_IDLE_ANGRY_BATON;
+int ACT_PUSH_PLAYER;
+int ACT_MELEE_ATTACK_THRUST;
+int ACT_ACTIVATE_BATON;
+int ACT_DEACTIVATE_BATON;
+
+LINK_ENTITY_TO_CLASS( npc_metropolice, CNPC_MetroPolice );
+
+BEGIN_DATADESC( CNPC_MetroPolice )
+
+ DEFINE_EMBEDDED( m_BatonSwingTimer ),
+ DEFINE_EMBEDDED( m_NextChargeTimer ),
+ DEFINE_FIELD( m_flBatonDebounceTime, FIELD_FLOAT ),
+ DEFINE_FIELD( m_bShouldActivateBaton, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_iPistolClips, FIELD_INTEGER ),
+ DEFINE_KEYFIELD( m_fWeaponDrawn, FIELD_BOOLEAN, "weapondrawn" ),
+ DEFINE_FIELD( m_LastShootSlot, FIELD_INTEGER ),
+ DEFINE_EMBEDDED( m_TimeYieldShootSlot ),
+ DEFINE_EMBEDDED( m_Sentences ),
+ DEFINE_FIELD( m_bPlayerIsNear, FIELD_BOOLEAN ),
+
+ DEFINE_FIELD( m_vecBurstTargetPos, FIELD_POSITION_VECTOR ),
+ DEFINE_FIELD( m_vecBurstDelta, FIELD_VECTOR ),
+ DEFINE_FIELD( m_nBurstHits, FIELD_INTEGER ),
+ DEFINE_FIELD( m_nMaxBurstHits, FIELD_INTEGER ),
+ DEFINE_FIELD( m_flBurstPredictTime, FIELD_TIME ),
+ DEFINE_FIELD( m_nBurstReloadCount, FIELD_INTEGER ),
+ DEFINE_FIELD( m_vecBurstLineOfDeathDelta, FIELD_VECTOR ),
+ DEFINE_FIELD( m_vecBurstLineOfDeathOrigin, FIELD_POSITION_VECTOR ),
+ DEFINE_FIELD( m_flBurstSteerDistance, FIELD_FLOAT ),
+ DEFINE_FIELD( m_nBurstMode, FIELD_INTEGER ),
+ DEFINE_FIELD( m_nBurstSteerMode, FIELD_INTEGER ),
+ DEFINE_FIELD( m_vecBurstPredictedVelocityDir, FIELD_VECTOR ),
+ DEFINE_FIELD( m_vecBurstPredictedSpeed, FIELD_FLOAT ),
+ DEFINE_FIELD( m_flValidStitchTime, FIELD_TIME ),
+ DEFINE_FIELD( m_flNextLedgeCheckTime, FIELD_TIME ),
+ DEFINE_FIELD( m_flTaskCompletionTime, FIELD_TIME ),
+ DEFINE_FIELD( m_flLastPhysicsFlinchTime, FIELD_TIME ),
+ DEFINE_FIELD( m_flLastDamageFlinchTime, FIELD_TIME ),
+
+ DEFINE_FIELD( m_hManhack, FIELD_EHANDLE ),
+ DEFINE_FIELD( m_hBlockingProp, FIELD_EHANDLE ),
+
+ DEFINE_FIELD( m_nRecentDamage, FIELD_INTEGER ),
+ DEFINE_FIELD( m_flRecentDamageTime, FIELD_TIME ),
+
+ DEFINE_FIELD( m_flNextPainSoundTime, FIELD_TIME ),
+ DEFINE_FIELD( m_flNextLostSoundTime, FIELD_TIME ),
+ DEFINE_FIELD( m_nIdleChatterType, FIELD_INTEGER ),
+
+ DEFINE_FIELD( m_bSimpleCops, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_flLastHitYaw, FIELD_FLOAT ),
+
+ DEFINE_FIELD( m_bPlayerTooClose, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_bKeepFacingPlayer, FIELD_BOOLEAN ),
+ DEFINE_FIELD( m_flChasePlayerTime, FIELD_TIME ),
+ DEFINE_FIELD( m_vecPreChaseOrigin, FIELD_VECTOR ),
+ DEFINE_FIELD( m_flPreChaseYaw, FIELD_FLOAT ),
+ DEFINE_FIELD( m_nNumWarnings, FIELD_INTEGER ),
+ DEFINE_FIELD( m_iNumPlayerHits, FIELD_INTEGER ),
+
+ // m_ActBusyBehavior (auto saved by AI)
+ // m_StandoffBehavior (auto saved by AI)
+ // m_AssaultBehavior (auto saved by AI)
+ // m_FuncTankBehavior (auto saved by AI)
+ // m_RappelBehavior (auto saved by AI)
+ // m_PolicingBehavior (auto saved by AI)
+ // m_FollowBehavior (auto saved by AI)
+
+ DEFINE_KEYFIELD( m_iManhacks, FIELD_INTEGER, "manhacks" ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "EnableManhackToss", InputEnableManhackToss ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetPoliceGoal", InputSetPoliceGoal ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "ActivateBaton", InputActivateBaton ),
+
+ DEFINE_USEFUNC( PrecriminalUse ),
+
+ DEFINE_OUTPUT( m_OnStunnedPlayer, "OnStunnedPlayer" ),
+ DEFINE_OUTPUT( m_OnCupCopped, "OnCupCopped" ),
+
+END_DATADESC()
+
+//------------------------------------------------------------------------------
+
+float CNPC_MetroPolice::gm_flTimeLastSpokePeek;
+
+//------------------------------------------------------------------------------
+// Purpose
+//------------------------------------------------------------------------------
+CBaseEntity *CNPC_MetroPolice::CheckTraceHullAttack( float flDist, const Vector &mins, const Vector &maxs, int iDamage, int iDmgType, float forceScale, bool bDamageAnyNPC )
+{
+ // If only a length is given assume we want to trace in our facing direction
+ Vector forward;
+ AngleVectors( GetAbsAngles(), &forward );
+ Vector vStart = GetAbsOrigin();
+
+ // The ideal place to start the trace is in the center of the attacker's bounding box.
+ // however, we need to make sure there's enough clearance. Some of the smaller monsters aren't
+ // as big as the hull we try to trace with. (SJB)
+ float flVerticalOffset = WorldAlignSize().z * 0.5;
+
+ if( flVerticalOffset < maxs.z )
+ {
+ // There isn't enough room to trace this hull, it's going to drag the ground.
+ // so make the vertical offset just enough to clear the ground.
+ flVerticalOffset = maxs.z + 1.0;
+ }
+
+ vStart.z += flVerticalOffset;
+ Vector vEnd = vStart + (forward * flDist );
+ return CheckTraceHullAttack( vStart, vEnd, mins, maxs, iDamage, iDmgType, forceScale, bDamageAnyNPC );
+}
+
+//------------------------------------------------------------------------------
+// Melee filter for police
+//------------------------------------------------------------------------------
+class CTraceFilterMetroPolice : public CTraceFilterEntitiesOnly
+{
+public:
+ // It does have a base, but we'll never network anything below here..
+ DECLARE_CLASS_NOBASE( CTraceFilterMetroPolice );
+
+ CTraceFilterMetroPolice( const IHandleEntity *passentity, int collisionGroup, CTakeDamageInfo *dmgInfo, float flForceScale, bool bDamageAnyNPC )
+ : m_pPassEnt(passentity), m_collisionGroup(collisionGroup), m_dmgInfo(dmgInfo), m_pHit(NULL), m_flForceScale(flForceScale), m_bDamageAnyNPC(bDamageAnyNPC)
+ {
+ }
+
+ virtual bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask )
+ {
+ if ( !StandardFilterRules( pHandleEntity, contentsMask ) )
+ return false;
+
+ if ( !PassServerEntityFilter( pHandleEntity, m_pPassEnt ) )
+ return false;
+
+ // Don't test if the game code tells us we should ignore this collision...
+ CBaseEntity *pEntity = EntityFromEntityHandle( pHandleEntity );
+
+ if ( pEntity )
+ {
+ if ( !pEntity->ShouldCollide( m_collisionGroup, contentsMask ) )
+ return false;
+
+ if ( !g_pGameRules->ShouldCollide( m_collisionGroup, pEntity->GetCollisionGroup() ) )
+ return false;
+
+ if ( pEntity->m_takedamage == DAMAGE_NO )
+ return false;
+
+ // Translate the vehicle into its driver for damage
+ if ( pEntity->GetServerVehicle() != NULL )
+ {
+ CBaseEntity *pDriver = pEntity->GetServerVehicle()->GetPassenger();
+
+ if ( pDriver != NULL )
+ {
+ pEntity = pDriver;
+ }
+ }
+
+ Vector attackDir = pEntity->WorldSpaceCenter() - m_dmgInfo->GetAttacker()->WorldSpaceCenter();
+ VectorNormalize( attackDir );
+
+ CTakeDamageInfo info = (*m_dmgInfo);
+ CalculateMeleeDamageForce( &info, attackDir, info.GetAttacker()->WorldSpaceCenter(), m_flForceScale );
+
+ if( !(pEntity->GetFlags() & FL_ONGROUND) )
+ {
+ // Don't hit airborne entities so hard. They fly farther since
+ // there's no friction with the ground.
+ info.ScaleDamageForce( 0.001 );
+ }
+
+ CBaseCombatCharacter *pBCC = info.GetAttacker()->MyCombatCharacterPointer();
+ CBaseCombatCharacter *pVictimBCC = pEntity->MyCombatCharacterPointer();
+
+ // Only do these comparisons between NPCs
+ if ( pBCC && pVictimBCC )
+ {
+ // Can only damage other NPCs that we hate
+ if ( m_bDamageAnyNPC || pBCC->IRelationType( pEntity ) == D_HT || pEntity->IsPlayer() )
+ {
+ if ( info.GetDamage() )
+ {
+ // If gordon's a criminal, do damage now
+ if ( !pEntity->IsPlayer() || GlobalEntity_GetState( "gordon_precriminal" ) == GLOBAL_OFF )
+ {
+ if ( pEntity->IsPlayer() && ((CBasePlayer *)pEntity)->IsSuitEquipped() )
+ {
+ info.ScaleDamage( .25 );
+ info.ScaleDamageForce( .25 );
+ }
+
+ pEntity->TakeDamage( info );
+ }
+ }
+
+ m_pHit = pEntity;
+ return true;
+ }
+ }
+ else
+ {
+ // Make sure if the player is holding this, he drops it
+ Pickup_ForcePlayerToDropThisObject( pEntity );
+
+ // Otherwise just damage passive objects in our way
+ if ( info.GetDamage() )
+ {
+ pEntity->TakeDamage( info );
+ }
+ }
+ }
+
+ return false;
+ }
+
+public:
+ const IHandleEntity *m_pPassEnt;
+ int m_collisionGroup;
+ CTakeDamageInfo *m_dmgInfo;
+ CBaseEntity *m_pHit;
+ float m_flForceScale;
+ bool m_bDamageAnyNPC;
+};
+
+//------------------------------------------------------------------------------
+// Purpose : start and end trace position, amount
+// of damage to do, and damage type. Returns a pointer to
+// the damaged entity in case the NPC wishes to do
+// other stuff to the victim (punchangle, etc)
+//
+// Used for many contact-range melee attacks. Bites, claws, etc.
+// Input :
+// Output :
+//------------------------------------------------------------------------------
+CBaseEntity *CNPC_MetroPolice::CheckTraceHullAttack( const Vector &vStart, const Vector &vEnd, const Vector &mins, const Vector &maxs, int iDamage, int iDmgType, float flForceScale, bool bDamageAnyNPC )
+{
+
+ CTakeDamageInfo dmgInfo( this, this, iDamage, DMG_SLASH );
+
+ CTraceFilterMetroPolice traceFilter( this, COLLISION_GROUP_NONE, &dmgInfo, flForceScale, bDamageAnyNPC );
+
+ Ray_t ray;
+ ray.Init( vStart, vEnd, mins, maxs );
+
+ trace_t tr;
+ enginetrace->TraceRay( ray, MASK_SHOT, &traceFilter, &tr );
+
+ CBaseEntity *pEntity = traceFilter.m_pHit;
+
+ if ( pEntity == NULL )
+ {
+ // See if perhaps I'm trying to claw/bash someone who is standing on my head.
+ Vector vecTopCenter;
+ Vector vecEnd;
+ Vector vecMins, vecMaxs;
+
+ // Do a tracehull from the top center of my bounding box.
+ vecTopCenter = GetAbsOrigin();
+ CollisionProp()->WorldSpaceAABB( &vecMins, &vecMaxs );
+ vecTopCenter.z = vecMaxs.z + 1.0f;
+ vecEnd = vecTopCenter;
+ vecEnd.z += 2.0f;
+
+ ray.Init( vecTopCenter, vEnd, mins, maxs );
+ enginetrace->TraceRay( ray, MASK_SHOT_HULL, &traceFilter, &tr );
+
+ pEntity = traceFilter.m_pHit;
+ }
+
+ return pEntity;
+}
+
+//-----------------------------------------------------------------------------
+// My buddies got killed!
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::NotifyDeadFriend( CBaseEntity* pFriend )
+{
+ BaseClass::NotifyDeadFriend(pFriend);
+
+ if ( pFriend == m_hManhack )
+ {
+ m_Sentences.Speak( "METROPOLICE_MANHACK_KILLED", SENTENCE_PRIORITY_NORMAL, SENTENCE_CRITERIA_NORMAL );
+ DevMsg("My manhack died!\n");
+ m_hManhack = NULL;
+ return;
+ }
+
+ // No notifications for squadmates' dead manhacks
+ if ( FClassnameIs( pFriend, "npc_manhack" ) )
+ return;
+
+ // Reset idle chatter, we may never get a response back
+ if ( m_nIdleChatterType == METROPOLICE_CHATTER_WAIT_FOR_RESPONSE )
+ {
+ m_nIdleChatterType = METROPOLICE_CHATTER_ASK_QUESTION;
+ }
+
+ if ( GetSquad()->NumMembers() < 2 )
+ {
+ m_Sentences.Speak( "METROPOLICE_LAST_OF_SQUAD", SENTENCE_PRIORITY_MEDIUM, SENTENCE_CRITERIA_NORMAL );
+ return;
+ }
+
+ m_Sentences.Speak( "METROPOLICE_MAN_DOWN", SENTENCE_PRIORITY_MEDIUM );
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+CNPC_MetroPolice::CNPC_MetroPolice()
+{
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::OnScheduleChange()
+{
+ BaseClass::OnScheduleChange();
+
+ if ( GetEnemy() && HasCondition( COND_ENEMY_DEAD ) )
+ {
+ AnnounceEnemyKill( GetEnemy() );
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::PrescheduleThink( void )
+{
+ BaseClass::PrescheduleThink();
+
+ // Speak any queued sentences
+ m_Sentences.UpdateSentenceQueue();
+
+ // Look at near players, always
+ m_bPlayerIsNear = false;
+ if ( PlayerIsCriminal() == false )
+ {
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 );
+
+ if ( pPlayer && ( pPlayer->WorldSpaceCenter() - WorldSpaceCenter() ).LengthSqr() < (128*128) )
+ {
+ m_bPlayerIsNear = true;
+ AddLookTarget( pPlayer, 0.75f, 5.0f );
+
+ if ( ( m_PolicingBehavior.IsEnabled() == false ) && ( m_nNumWarnings >= METROPOLICE_MAX_WARNINGS ) )
+ {
+ m_flBatonDebounceTime = gpGlobals->curtime + random->RandomFloat( 2.5f, 4.0f );
+ SetTarget( pPlayer );
+ SetBatonState( true );
+ }
+ }
+ else
+ {
+ if ( m_PolicingBehavior.IsEnabled() == false && gpGlobals->curtime > m_flBatonDebounceTime )
+ {
+ SetBatonState( false );
+ }
+
+ m_bKeepFacingPlayer = false;
+ }
+ }
+
+ if( IsOnFire() )
+ {
+ SetCondition( COND_METROPOLICE_ON_FIRE );
+ }
+ else
+ {
+ ClearCondition( COND_METROPOLICE_ON_FIRE );
+ }
+
+ if (gpGlobals->curtime > m_flRecentDamageTime + RECENT_DAMAGE_INTERVAL)
+ {
+ m_nRecentDamage = 0;
+ m_flRecentDamageTime = 0;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &move -
+// flInterval -
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::OverrideMoveFacing( const AILocalMoveGoal_t &move, float flInterval )
+{
+ // Don't do this if we're scripted
+ if ( IsInAScript() )
+ return BaseClass::OverrideMoveFacing( move, flInterval );
+
+ // ROBIN: Disabled at request of mapmakers for now
+ /*
+ // If we're moving during a police sequence, always face our target
+ if ( m_PolicingBehavior.IsEnabled() )
+ {
+ CBaseEntity *pTarget = m_PolicingBehavior.GetGoalTarget();
+
+ if ( pTarget )
+ {
+ AddFacingTarget( pTarget, pTarget->WorldSpaceCenter(), 1.0f, 0.2f );
+ }
+ }
+ */
+
+ return BaseClass::OverrideMoveFacing( move, flInterval );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::Precache( void )
+{
+ if ( HasSpawnFlags( SF_NPC_START_EFFICIENT ) )
+ {
+ SetModelName( AllocPooledString("models/police_cheaple.mdl" ) );
+ }
+ else
+ {
+ SetModelName( AllocPooledString("models/police.mdl") );
+ }
+
+ PrecacheModel( STRING( GetModelName() ) );
+
+ UTIL_PrecacheOther( "npc_manhack" );
+
+ PrecacheScriptSound( "NPC_Metropolice.Shove" );
+ PrecacheScriptSound( "NPC_MetroPolice.WaterSpeech" );
+ PrecacheScriptSound( "NPC_MetroPolice.HidingSpeech" );
+ enginesound->PrecacheSentenceGroup( "METROPOLICE" );
+
+ BaseClass::Precache();
+}
+
+
+//-----------------------------------------------------------------------------
+// Create components
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::CreateComponents()
+{
+ if ( !BaseClass::CreateComponents() )
+ return false;
+
+ m_Sentences.Init( this, "NPC_Metropolice.SentenceParameters" );
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//
+//
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::Spawn( void )
+{
+ Precache();
+
+#ifdef _XBOX
+ // Always fade the corpse
+ AddSpawnFlags( SF_NPC_FADE_CORPSE );
+#endif // _XBOX
+
+ SetModel( STRING( GetModelName() ) );
+
+ SetHullType(HULL_HUMAN);
+ SetHullSizeNormal();
+
+ SetSolid( SOLID_BBOX );
+ AddSolidFlags( FSOLID_NOT_STANDABLE );
+ SetMoveType( MOVETYPE_STEP );
+ SetBloodColor( BLOOD_COLOR_RED );
+ m_nIdleChatterType = METROPOLICE_CHATTER_ASK_QUESTION;
+ m_bSimpleCops = HasSpawnFlags( SF_METROPOLICE_SIMPLE_VERSION );
+ if ( HasSpawnFlags( SF_METROPOLICE_NOCHATTER ) )
+ {
+ AddSpawnFlags( SF_NPC_GAG );
+ }
+
+ if (!m_bSimpleCops)
+ {
+ m_iHealth = sk_metropolice_health.GetFloat();
+ }
+ else
+ {
+ m_iHealth = sk_metropolice_simple_health.GetFloat();
+ }
+
+ m_flFieldOfView = -0.2;// indicates the width of this NPC's forward view cone ( as a dotproduct result )
+ m_NPCState = NPC_STATE_NONE;
+ if ( !HasSpawnFlags( SF_NPC_START_EFFICIENT ) )
+ {
+ CapabilitiesAdd( bits_CAP_TURN_HEAD | bits_CAP_ANIMATEDFACE );
+ CapabilitiesAdd( bits_CAP_AIM_GUN | bits_CAP_MOVE_SHOOT );
+ }
+ CapabilitiesAdd( bits_CAP_MOVE_GROUND );
+ CapabilitiesAdd( bits_CAP_USE_WEAPONS | bits_CAP_NO_HIT_SQUADMATES );
+ CapabilitiesAdd( bits_CAP_SQUAD );
+ CapabilitiesAdd( bits_CAP_DUCK | bits_CAP_DOORS_GROUP );
+ CapabilitiesAdd( bits_CAP_USE_SHOT_REGULATOR );
+
+ m_nBurstHits = 0;
+ m_HackedGunPos = Vector ( 0, 0, 55 );
+
+ m_iPistolClips = METROPOLICE_NUM_CLIPS;
+
+ NPCInit();
+
+ // NOTE: This must occur *after* init, since init sets default dist look
+ if ( HasSpawnFlags( SF_METROPOLICE_MID_RANGE_ATTACK ) )
+ {
+ m_flDistTooFar = METROPOLICE_MID_RANGE_ATTACK_RANGE;
+ SetDistLook( METROPOLICE_MID_RANGE_ATTACK_RANGE );
+ }
+
+ m_hManhack = NULL;
+
+ if ( GetActiveWeapon() )
+ {
+ CBaseCombatWeapon *pWeapon;
+
+ pWeapon = GetActiveWeapon();
+
+ if( !FClassnameIs( pWeapon, "weapon_pistol" ) )
+ {
+ m_fWeaponDrawn = true;
+ }
+
+ if( !m_fWeaponDrawn )
+ {
+ GetActiveWeapon()->AddEffects( EF_NODRAW );
+ }
+ }
+
+
+ m_TimeYieldShootSlot.Set( 2, 6 );
+
+ GetEnemies()->SetFreeKnowledgeDuration( 6.0 );
+
+ m_bShouldActivateBaton = false;
+ m_flValidStitchTime = -1.0f;
+ m_flNextLedgeCheckTime = -1.0f;
+ m_nBurstReloadCount = METROPOLICE_BURST_RELOAD_COUNT;
+ SetBurstMode( false );
+
+ // Clear out spawnflag if we're missing the smg1
+ if( HasSpawnFlags( SF_METROPOLICE_ALWAYS_STITCH ) )
+ {
+ if ( !Weapon_OwnsThisType( "weapon_smg1" ) )
+ {
+ Warning( "Warning! Metrocop is trying to use the stitch behavior but he has no smg1!\n" );
+ RemoveSpawnFlags( SF_METROPOLICE_ALWAYS_STITCH );
+ }
+ }
+
+ m_nNumWarnings = 0;
+ m_bPlayerTooClose = false;
+ m_bKeepFacingPlayer = false;
+ m_flChasePlayerTime = 0;
+ m_vecPreChaseOrigin = vec3_origin;
+ m_flPreChaseYaw = 0;
+
+ SetUse( &CNPC_MetroPolice::PrecriminalUse );
+
+ // Start us with a visible manhack if we have one
+ if ( m_iManhacks )
+ {
+ SetBodygroup( METROPOLICE_BODYGROUP_MANHACK, true );
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Update weapon ranges
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::Weapon_Equip( CBaseCombatWeapon *pWeapon )
+{
+ BaseClass::Weapon_Equip( pWeapon );
+
+ if ( HasSpawnFlags(SF_METROPOLICE_MID_RANGE_ATTACK) && GetActiveWeapon() )
+ {
+ GetActiveWeapon()->m_fMaxRange1 = METROPOLICE_MID_RANGE_ATTACK_RANGE;
+ GetActiveWeapon()->m_fMaxRange2 = METROPOLICE_MID_RANGE_ATTACK_RANGE;
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// FuncTankBehavior-related sentences
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::SpeakFuncTankSentence( int nSentenceType )
+{
+ switch ( nSentenceType )
+ {
+ case FUNCTANK_SENTENCE_MOVE_TO_MOUNT:
+ m_Sentences.Speak( "METROPOLICE_FT_APPROACH", SENTENCE_PRIORITY_MEDIUM );
+ break;
+
+ case FUNCTANK_SENTENCE_JUST_MOUNTED:
+ m_Sentences.Speak( "METROPOLICE_FT_MOUNT", SENTENCE_PRIORITY_HIGH );
+ break;
+
+ case FUNCTANK_SENTENCE_SCAN_FOR_ENEMIES:
+ m_Sentences.Speak( "METROPOLICE_FT_SCAN", SENTENCE_PRIORITY_NORMAL );
+ break;
+
+ case FUNCTANK_SENTENCE_DISMOUNTING:
+ m_Sentences.Speak( "METROPOLICE_FT_DISMOUNT", SENTENCE_PRIORITY_HIGH );
+ break;
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Standoff Behavior-related sentences
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::SpeakStandoffSentence( int nSentenceType )
+{
+ switch ( nSentenceType )
+ {
+ case STANDOFF_SENTENCE_BEGIN_STANDOFF:
+ m_Sentences.Speak( "METROPOLICE_SO_BEGIN", SENTENCE_PRIORITY_HIGH, SENTENCE_CRITERIA_SQUAD_LEADER );
+ break;
+
+ case STANDOFF_SENTENCE_END_STANDOFF:
+ m_Sentences.Speak( "METROPOLICE_SO_END", SENTENCE_PRIORITY_HIGH, SENTENCE_CRITERIA_SQUAD_LEADER );
+ break;
+
+ case STANDOFF_SENTENCE_OUT_OF_AMMO:
+ AnnounceOutOfAmmo( );
+ break;
+
+ case STANDOFF_SENTENCE_FORCED_TAKE_COVER:
+ m_Sentences.Speak( "METROPOLICE_SO_FORCE_COVER" );
+ break;
+
+ case STANDOFF_SENTENCE_STAND_CHECK_TARGET:
+ if ( gm_flTimeLastSpokePeek != 0 && gpGlobals->curtime - gm_flTimeLastSpokePeek > 20 )
+ {
+ m_Sentences.Speak( "METROPOLICE_SO_PEEK" );
+ gm_flTimeLastSpokePeek = gpGlobals->curtime;
+ }
+ break;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Assault Behavior-related sentences
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::SpeakAssaultSentence( int nSentenceType )
+{
+ switch ( nSentenceType )
+ {
+ case ASSAULT_SENTENCE_HIT_RALLY_POINT:
+ m_Sentences.SpeakQueued( "METROPOLICE_AS_HIT_RALLY", SENTENCE_PRIORITY_NORMAL );
+ break;
+
+ case ASSAULT_SENTENCE_HIT_ASSAULT_POINT:
+ m_Sentences.SpeakQueued( "METROPOLICE_AS_HIT_ASSAULT", SENTENCE_PRIORITY_NORMAL );
+ break;
+
+ case ASSAULT_SENTENCE_SQUAD_ADVANCE_TO_RALLY:
+ if ( m_Sentences.Speak( "METROPOLICE_AS_ADV_RALLY", SENTENCE_PRIORITY_MEDIUM, SENTENCE_CRITERIA_SQUAD_LEADER ) >= 0 )
+ {
+ GetSquad()->BroadcastInteraction( g_interactionMetrocopClearSentenceQueues, NULL );
+ }
+ break;
+
+ case ASSAULT_SENTENCE_SQUAD_ADVANCE_TO_ASSAULT:
+ if ( m_Sentences.Speak( "METROPOLICE_AS_ADV_ASSAULT", SENTENCE_PRIORITY_MEDIUM, SENTENCE_CRITERIA_SQUAD_LEADER ) >= 0 )
+ {
+ GetSquad()->BroadcastInteraction( g_interactionMetrocopClearSentenceQueues, NULL );
+ }
+ break;
+
+ case ASSAULT_SENTENCE_COVER_NO_AMMO:
+ AnnounceOutOfAmmo( );
+ break;
+
+ case ASSAULT_SENTENCE_UNDER_ATTACK:
+ m_Sentences.Speak( "METROPOLICE_GO_ALERT" );
+ break;
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Speaking while using TASK_SPEAK_SENTENCE
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::SpeakSentence( int nSentenceType )
+{
+ if ( !PlayerIsCriminal() )
+ return;
+
+ if ( nSentenceType >= SENTENCE_BASE_BEHAVIOR_INDEX )
+ {
+ if ( GetRunningBehavior() == &m_FuncTankBehavior )
+ {
+ SpeakFuncTankSentence( nSentenceType );
+ return;
+ }
+
+ if ( GetRunningBehavior() == &m_StandoffBehavior )
+ {
+ SpeakStandoffSentence( nSentenceType );
+ return;
+ }
+
+ if ( GetRunningBehavior() == &m_AssaultBehavior )
+ {
+ SpeakAssaultSentence( nSentenceType );
+ return;
+ }
+ }
+
+ switch ( nSentenceType )
+ {
+ case METROPOLICE_SENTENCE_FREEZE:
+ m_Sentences.Speak( "METROPOLICE_FREEZE", SENTENCE_PRIORITY_MEDIUM, SENTENCE_CRITERIA_NORMAL );
+ break;
+
+ case METROPOLICE_SENTENCE_HES_OVER_HERE:
+ m_Sentences.Speak( "METROPOLICE_OVER_HERE", SENTENCE_PRIORITY_MEDIUM, SENTENCE_CRITERIA_NORMAL );
+ break;
+
+ case METROPOLICE_SENTENCE_HES_RUNNING:
+ m_Sentences.Speak( "METROPOLICE_HES_RUNNING", SENTENCE_PRIORITY_HIGH, SENTENCE_CRITERIA_NORMAL );
+ break;
+
+ case METROPOLICE_SENTENCE_TAKE_HIM_DOWN:
+ m_Sentences.Speak( "METROPOLICE_TAKE_HIM_DOWN", SENTENCE_PRIORITY_HIGH, SENTENCE_CRITERIA_NORMAL );
+ break;
+
+ case METROPOLICE_SENTENCE_ARREST_IN_POSITION:
+ m_Sentences.Speak( "METROPOLICE_ARREST_IN_POS", SENTENCE_PRIORITY_MEDIUM, SENTENCE_CRITERIA_NORMAL );
+ break;
+
+ case METROPOLICE_SENTENCE_DEPLOY_MANHACK:
+ m_Sentences.Speak( "METROPOLICE_DEPLOY_MANHACK" );
+ break;
+
+ case METROPOLICE_SENTENCE_MOVE_INTO_POSITION:
+ {
+ CBaseEntity *pEntity = GetEnemy();
+
+ // NOTE: This is a good time to check to see if the player is hurt.
+ // Have the cops notice this and call out
+ if ( pEntity && !HasSpawnFlags( SF_METROPOLICE_ARREST_ENEMY ) )
+ {
+ if ( pEntity->IsPlayer() && (pEntity->GetHealth() <= 20) )
+ {
+ if ( !HasMemory(bits_MEMORY_PLAYER_HURT) )
+ {
+ if ( m_Sentences.Speak( "METROPOLICE_PLAYERHIT", SENTENCE_PRIORITY_HIGH ) >= 0 )
+ {
+ m_pSquad->SquadRemember(bits_MEMORY_PLAYER_HURT);
+ }
+ }
+ }
+
+ if ( GetNavigator()->GetPath()->GetPathLength() > 20 * 12.0f )
+ {
+ m_Sentences.Speak( "METROPOLICE_FLANK" );
+ }
+ }
+ }
+ break;
+
+ case METROPOLICE_SENTENCE_HEARD_SOMETHING:
+ if ( ( GetState() == NPC_STATE_ALERT ) || ( GetState() == NPC_STATE_IDLE ) )
+ {
+ m_Sentences.Speak( "METROPOLICE_HEARD_SOMETHING", SENTENCE_PRIORITY_MEDIUM );
+ }
+ break;
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Speaking
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::AnnounceEnemyType( CBaseEntity *pEnemy )
+{
+ if ( !pEnemy || !m_pSquad )
+ return;
+
+ // Don't announce enemies when the player isn't a criminal
+ if ( !PlayerIsCriminal() )
+ return;
+
+ // Don't announce enemies when I'm in arrest behavior
+ if ( HasSpawnFlags( SF_METROPOLICE_ARREST_ENEMY ) )
+ return;
+
+ if ( m_pSquad->IsLeader( this ) || ( m_pSquad->GetLeader() && m_pSquad->GetLeader()->GetEnemy() != GetEnemy() ) )
+ {
+ // First contact, and I'm the squad leader.
+ const char *pSentenceName = "METROPOLICE_MONST";
+ switch ( pEnemy->Classify() )
+ {
+ case CLASS_PLAYER:
+ {
+ CBasePlayer *pPlayer = assert_cast<CBasePlayer*>( pEnemy );
+ if ( pPlayer && pPlayer->IsInAVehicle() )
+ {
+ pSentenceName = "METROPOLICE_MONST_PLAYER_VEHICLE";
+ }
+ else
+ {
+ pSentenceName = "METROPOLICE_MONST_PLAYER";
+ }
+ }
+ break;
+
+ case CLASS_PLAYER_ALLY:
+ case CLASS_CITIZEN_REBEL:
+ case CLASS_CITIZEN_PASSIVE:
+ case CLASS_VORTIGAUNT:
+ pSentenceName = "METROPOLICE_MONST_CITIZENS";
+ break;
+
+ case CLASS_PLAYER_ALLY_VITAL:
+ pSentenceName = "METROPOLICE_MONST_CHARACTER";
+ break;
+
+ case CLASS_ANTLION:
+ pSentenceName = "METROPOLICE_MONST_BUGS";
+ break;
+
+ case CLASS_ZOMBIE:
+ pSentenceName = "METROPOLICE_MONST_ZOMBIES";
+ break;
+
+ case CLASS_HEADCRAB:
+ case CLASS_BARNACLE:
+ pSentenceName = "METROPOLICE_MONST_PARASITES";
+ break;
+ }
+
+ m_Sentences.Speak( pSentenceName, SENTENCE_PRIORITY_HIGH );
+ }
+ else
+ {
+ if ( m_pSquad->GetLeader() && FOkToMakeSound( SENTENCE_PRIORITY_MEDIUM ) )
+ {
+ // squelch anything that isn't high priority so the leader can speak
+ JustMadeSound( SENTENCE_PRIORITY_MEDIUM );
+ }
+ }
+
+}
+
+
+//-----------------------------------------------------------------------------
+// Speaking
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::AnnounceEnemyKill( CBaseEntity *pEnemy )
+{
+ if ( !pEnemy )
+ return;
+
+ const char *pSentenceName = "METROPOLICE_KILL_MONST";
+ switch ( pEnemy->Classify() )
+ {
+ case CLASS_PLAYER:
+ pSentenceName = "METROPOLICE_KILL_PLAYER";
+ break;
+
+ // no sentences for these guys yet
+ case CLASS_PLAYER_ALLY:
+ case CLASS_CITIZEN_REBEL:
+ case CLASS_CITIZEN_PASSIVE:
+ case CLASS_VORTIGAUNT:
+ pSentenceName = "METROPOLICE_KILL_CITIZENS";
+ break;
+
+ case CLASS_PLAYER_ALLY_VITAL:
+ pSentenceName = "METROPOLICE_KILL_CHARACTER";
+ break;
+
+ case CLASS_ANTLION:
+ pSentenceName = "METROPOLICE_KILL_BUGS";
+ break;
+
+ case CLASS_ZOMBIE:
+ pSentenceName = "METROPOLICE_KILL_ZOMBIES";
+ break;
+
+ case CLASS_HEADCRAB:
+ case CLASS_BARNACLE:
+ pSentenceName = "METROPOLICE_KILL_PARASITES";
+ break;
+ }
+
+ m_Sentences.Speak( pSentenceName, SENTENCE_PRIORITY_HIGH );
+}
+
+
+//-----------------------------------------------------------------------------
+// Announce out of ammo
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::AnnounceOutOfAmmo( )
+{
+ if ( HasCondition( COND_NO_PRIMARY_AMMO ) )
+ {
+ m_Sentences.Speak( "METROPOLICE_COVER_NO_AMMO" );
+ }
+ else
+ {
+ m_Sentences.Speak( "METROPOLICE_COVER_LOW_AMMO" );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// We're taking cover from danger
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::AnnounceTakeCoverFromDanger( CSound *pSound )
+{
+ CBaseEntity *pSoundOwner = pSound->m_hOwner;
+ if ( pSoundOwner )
+ {
+ CBaseGrenade *pGrenade = dynamic_cast<CBaseGrenade *>(pSoundOwner);
+ if ( pGrenade )
+ {
+ if ( IRelationType( pGrenade->GetThrower() ) != D_LI )
+ {
+ // special case call out for enemy grenades
+ m_Sentences.Speak( "METROPOLICE_DANGER_GREN", SENTENCE_PRIORITY_HIGH, SENTENCE_CRITERIA_NORMAL );
+ }
+ return;
+ }
+
+ if ( pSoundOwner->GetServerVehicle() )
+ {
+ m_Sentences.Speak( "METROPOLICE_DANGER_VEHICLE", SENTENCE_PRIORITY_HIGH, SENTENCE_CRITERIA_NORMAL );
+ return;
+ }
+
+ if ( FClassnameIs( pSoundOwner, "npc_manhack" ) )
+ {
+ if ( pSoundOwner->HasPhysicsAttacker( 1.0f ) )
+ {
+ m_Sentences.Speak( "METROPOLICE_DANGER_MANHACK", SENTENCE_PRIORITY_HIGH, SENTENCE_CRITERIA_NORMAL );
+ }
+ return;
+ }
+ }
+
+ // I hear something dangerous, probably need to take cover.
+ // dangerous sound nearby!, call it out
+ const char *pSentenceName = "METROPOLICE_DANGER";
+ m_Sentences.Speak( pSentenceName, SENTENCE_PRIORITY_HIGH, SENTENCE_CRITERIA_NORMAL );
+}
+
+
+
+//-----------------------------------------------------------------------------
+// Are we currently firing a burst?
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::IsCurrentlyFiringBurst() const
+{
+ return (m_nBurstMode != BURST_NOT_ACTIVE);
+}
+
+
+//-----------------------------------------------------------------------------
+// Is my enemy currently in an airboat?
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::IsEnemyInAnAirboat() const
+{
+ // Should this be a condition??
+ if ( !GetEnemy() || !GetEnemy()->IsPlayer() )
+ return false;
+
+ CBaseEntity *pVehicle = static_cast<CBasePlayer*>( GetEnemy() )->GetVehicleEntity();
+ if ( !pVehicle )
+ return false;
+
+ // NOTE: Could just return true if in a vehicle maybe
+ return FClassnameIs( pVehicle, "prop_vehicle_airboat" );
+}
+
+
+//-----------------------------------------------------------------------------
+// Returns the airboat
+//-----------------------------------------------------------------------------
+CBaseEntity *CNPC_MetroPolice::GetEnemyAirboat() const
+{
+ // Should this be a condition??
+ if ( !GetEnemy() || !GetEnemy()->IsPlayer() )
+ return NULL;
+
+ return static_cast<CBasePlayer*>( GetEnemy() )->GetVehicleEntity();
+}
+
+
+//-----------------------------------------------------------------------------
+// Which entity are we actually trying to shoot at?
+//-----------------------------------------------------------------------------
+CBaseEntity *CNPC_MetroPolice::GetShootTarget()
+{
+ // Should this be a condition??
+ CBaseEntity *pEnemy = GetEnemy();
+ if ( !pEnemy || !pEnemy->IsPlayer() )
+ return pEnemy;
+
+ CBaseEntity *pVehicle = static_cast<CBasePlayer*>( pEnemy )->GetVehicleEntity();
+ return pVehicle ? pVehicle : pEnemy;
+}
+
+
+//-----------------------------------------------------------------------------
+// Set up the shot regulator based on the equipped weapon
+//-----------------------------------------------------------------------------
+
+// Ranges across which to tune fire rates
+const float MIN_PISTOL_MODIFY_DIST = 15 * 12;
+const float MAX_PISTOL_MODIFY_DIST = 150 * 12;
+
+// Range for rest period minimums
+const float MIN_MIN_PISTOL_REST_INTERVAL = 0.6;
+const float MAX_MIN_PISTOL_REST_INTERVAL = 1.2;
+
+// Range for rest period maximums
+const float MIN_MAX_PISTOL_REST_INTERVAL = 1.2;
+const float MAX_MAX_PISTOL_REST_INTERVAL = 2.0;
+
+// Range for burst minimums
+const int MIN_MIN_PISTOL_BURST = 2;
+const int MAX_MIN_PISTOL_BURST = 4;
+
+// Range for burst maximums
+const int MIN_MAX_PISTOL_BURST = 5;
+const int MAX_MAX_PISTOL_BURST = 8;
+
+void CNPC_MetroPolice::OnUpdateShotRegulator( )
+{
+ BaseClass::OnUpdateShotRegulator();
+
+ // FIXME: This code (except the burst interval) could be used for all weapon types
+ if( Weapon_OwnsThisType( "weapon_pistol" ) )
+ {
+ if ( m_nBurstMode == BURST_NOT_ACTIVE )
+ {
+ if ( GetEnemy() )
+ {
+ float dist = WorldSpaceCenter().DistTo( GetEnemy()->WorldSpaceCenter() );
+
+ dist = clamp( dist, MIN_PISTOL_MODIFY_DIST, MAX_PISTOL_MODIFY_DIST );
+
+ float factor = (dist - MIN_PISTOL_MODIFY_DIST) / (MAX_PISTOL_MODIFY_DIST - MIN_PISTOL_MODIFY_DIST);
+
+ int nMinBurst = MIN_MIN_PISTOL_BURST + ( MAX_MIN_PISTOL_BURST - MIN_MIN_PISTOL_BURST ) * (1.0 - factor);
+ int nMaxBurst = MIN_MAX_PISTOL_BURST + ( MAX_MAX_PISTOL_BURST - MIN_MAX_PISTOL_BURST ) * (1.0 - factor);
+ float flMinRestInterval = MIN_MIN_PISTOL_REST_INTERVAL + ( MAX_MIN_PISTOL_REST_INTERVAL - MIN_MIN_PISTOL_REST_INTERVAL ) * factor;
+ float flMaxRestInterval = MIN_MAX_PISTOL_REST_INTERVAL + ( MAX_MAX_PISTOL_REST_INTERVAL - MIN_MAX_PISTOL_REST_INTERVAL ) * factor;
+
+ GetShotRegulator()->SetRestInterval( flMinRestInterval, flMaxRestInterval );
+ GetShotRegulator()->SetBurstShotCountRange( nMinBurst, nMaxBurst );
+ }
+ else
+ {
+ GetShotRegulator()->SetBurstShotCountRange(GetActiveWeapon()->GetMinBurst(), GetActiveWeapon()->GetMaxBurst() );
+ GetShotRegulator()->SetRestInterval( 0.6, 1.4 );
+ }
+ }
+
+ // Add some noise into the pistol
+ GetShotRegulator()->SetBurstInterval( 0.2f, 0.5f );
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Burst mode!
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::SetBurstMode( bool bEnable )
+{
+ int nOldBurstMode = m_nBurstMode;
+ m_nBurstSteerMode = BURST_STEER_NONE;
+ m_flBurstPredictTime = gpGlobals->curtime - 1.0f;
+ if ( GetActiveWeapon() )
+ {
+ m_nBurstMode = bEnable ? BURST_ACTIVE : BURST_NOT_ACTIVE;
+ if ( bEnable )
+ {
+ m_nBurstHits = 0;
+ }
+ }
+ else
+ {
+ m_nBurstMode = BURST_NOT_ACTIVE;
+ }
+
+ if ( m_nBurstMode != nOldBurstMode )
+ {
+ OnUpdateShotRegulator();
+ if ( m_nBurstMode == BURST_NOT_ACTIVE )
+ {
+ // Check for inconsistency...
+ int nMinBurstCount, nMaxBurstCount;
+ GetShotRegulator()->GetBurstShotCountRange( &nMinBurstCount, &nMaxBurstCount );
+ if ( GetShotRegulator()->GetBurstShotsRemaining() > nMaxBurstCount )
+ {
+ GetShotRegulator()->SetBurstShotsRemaining( nMaxBurstCount );
+ }
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Should we attempt to stitch?
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::ShouldAttemptToStitch()
+{
+ if ( IsEnemyInAnAirboat() )
+ return true;
+
+ if ( !GetShootTarget() )
+ return false;
+
+ if ( HasSpawnFlags( SF_METROPOLICE_ALWAYS_STITCH ) )
+ {
+ // Don't stitch if the player is at the same level or higher
+ if ( GetEnemy()->GetAbsOrigin().z - GetAbsOrigin().z > -36 )
+ return false;
+
+ return true;
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------
+// position to shoot at
+//-----------------------------------------------------------------------------
+Vector CNPC_MetroPolice::StitchAimTarget( const Vector &posSrc, bool bNoisy )
+{
+ // This will make us aim a stitch at the feet of the player so we can see it
+ if ( !GetEnemy()->IsPlayer() )
+ return GetShootTarget()->BodyTarget( posSrc, bNoisy );
+
+ if ( !IsEnemyInAnAirboat() )
+ {
+ Vector vecBodyTarget;
+ if ( ( GetEnemy()->GetWaterLevel() == 0 ) && ( GetEnemy()->GetFlags() & FL_ONGROUND ) )
+ {
+ GetEnemy()->CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 0.08f ), &vecBodyTarget );
+ return vecBodyTarget;
+ }
+
+ // Underwater? Just use the normal thing
+ if ( GetEnemy()->GetWaterLevel() == 3 )
+ return GetShootTarget()->BodyTarget( posSrc, bNoisy );
+
+ // Trace down...
+ trace_t trace;
+ GetEnemy()->CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 1.0f ), &vecBodyTarget );
+ float flHeight = GetEnemy()->WorldAlignSize().z;
+ UTIL_TraceLine( vecBodyTarget, vecBodyTarget + Vector( 0, 0, -flHeight -80 ),
+ (MASK_SOLID_BRUSHONLY | MASK_WATER), NULL, COLLISION_GROUP_NONE, &trace );
+ return trace.endpos;
+ }
+
+ // NOTE: HACK! Ths 0.08 is where the water level happens to be.
+ // We probably want to find the exact water level and use that as the z position.
+ Vector vecBodyTarget;
+ if ( !bNoisy )
+ {
+ GetShootTarget()->CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 0.08f ), &vecBodyTarget );
+ }
+ else
+ {
+ GetShootTarget()->CollisionProp()->RandomPointInBounds( Vector( 0.25f, 0.25f, 0.08f ), Vector( 0.75f, 0.75f, 0.08f ), &vecBodyTarget );
+ }
+
+ return vecBodyTarget;
+}
+
+
+//-----------------------------------------------------------------------------
+// Burst mode!
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::AimBurstRandomly( int nMinCount, int nMaxCount, float flMinDelay, float flMaxDelay )
+{
+ if ( !IsCurrentlyFiringBurst() )
+ return;
+
+ GetShotRegulator()->SetParameters( nMinCount, nMaxCount, flMinDelay, flMaxDelay );
+ GetShotRegulator()->Reset( true );
+
+ int nShotCount = GetShotRegulator()->GetBurstShotsRemaining();
+
+ Vector vecDelta = StitchAimTarget( GetAbsOrigin(), true ) - Weapon_ShootPosition();
+ VectorNormalize( vecDelta );
+
+ // Choose a random direction vector perpendicular to the delta position
+ Vector vecRight, vecUp;
+ VectorVectors( vecDelta, vecRight, vecUp );
+ float flAngle = random->RandomFloat( 0.0f, 2 * M_PI );
+ VectorMultiply( vecRight, cos(flAngle), m_vecBurstDelta );
+ VectorMA( m_vecBurstDelta, sin(flAngle), vecUp, m_vecBurstDelta );
+
+ // The size of this determines the cone angle
+ m_vecBurstDelta *= 0.4f;
+
+ VectorMA( vecDelta, -0.5f, m_vecBurstDelta, m_vecBurstTargetPos );
+ m_vecBurstTargetPos += Weapon_ShootPosition();
+
+ m_vecBurstDelta /= (nShotCount - 1);
+}
+
+
+//-----------------------------------------------------------------------------
+// Choose a random vector somewhere between the two specified vectors
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::RandomDirectionBetweenVectors( const Vector &vecStart, const Vector &vecEnd, Vector *pResult )
+{
+ Assert( fabs( vecStart.Length() - 1.0f ) < 1e-3 );
+ Assert( fabs( vecEnd.Length() - 1.0f ) < 1e-3 );
+
+ float flCosAngle = DotProduct( vecStart, vecEnd );
+ if ( fabs( flCosAngle - 1.0f ) < 1e-3 )
+ {
+ *pResult = vecStart;
+ return;
+ }
+
+ Vector vecNormal;
+ CrossProduct( vecStart, vecEnd, vecNormal );
+ float flLength = VectorNormalize( vecNormal );
+ if ( flLength < 1e-3 )
+ {
+ // This is wrong for anti-parallel vectors. so what?
+ *pResult = vecStart;
+ return;
+ }
+
+ // Rotate the starting angle the specified amount
+ float flAngle = acos(flCosAngle) * random->RandomFloat( 0.0f, 1.0f );
+ VMatrix rotationMatrix;
+ MatrixBuildRotationAboutAxis( rotationMatrix, vecNormal, flAngle );
+ Vector3DMultiply( rotationMatrix, vecStart, *pResult );
+}
+
+
+//-----------------------------------------------------------------------------
+// Compute a predicted shoot target position n seconds into the future
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::PredictShootTargetPosition( float flDeltaTime, float flMinLeadDist, float flAddVelocity, Vector *pVecTarget, Vector *pVecTargetVelocity )
+{
+ CBaseEntity *pShootTarget = GetShootTarget();
+ *pVecTarget = StitchAimTarget( GetAbsOrigin(), true );
+
+ Vector vecSmoothedVel = pShootTarget->GetSmoothedVelocity();
+
+ // When we're in the air, don't predict vertical motion
+ if( (pShootTarget->GetFlags() & FL_ONGROUND) == 0 )
+ {
+ vecSmoothedVel.z = 0.0f;
+ }
+
+ Vector vecVelocity;
+ AngularImpulse angImpulse;
+ GetShootTarget()->GetVelocity( &vecVelocity, &angImpulse );
+
+ Vector vecLeadVector;
+ VMatrix rotationMatrix;
+ float flAngVel = VectorNormalize( angImpulse );
+ flAngVel -= 30.0f;
+ if ( flAngVel > 0.0f )
+ {
+ MatrixBuildRotationAboutAxis( rotationMatrix, angImpulse, flAngVel * flDeltaTime * 0.333f );
+ Vector3DMultiply( rotationMatrix, vecSmoothedVel, vecLeadVector );
+ }
+ else
+ {
+ vecLeadVector = vecSmoothedVel;
+ }
+
+ if ( flAddVelocity != 0.0f )
+ {
+ Vector vecForward;
+ pShootTarget->GetVectors( &vecForward, NULL, NULL );
+ VectorMA( vecLeadVector, flAddVelocity, vecForward, vecLeadVector );
+ }
+
+ *pVecTargetVelocity = vecLeadVector;
+
+ if ( (vecLeadVector.LengthSqr() * flDeltaTime * flDeltaTime) < flMinLeadDist * flMinLeadDist )
+ {
+ VectorNormalize( vecLeadVector );
+ vecLeadVector *= flMinLeadDist;
+ }
+ else
+ {
+ vecLeadVector *= flDeltaTime;
+ }
+
+ *pVecTarget += vecLeadVector;
+}
+
+
+//-----------------------------------------------------------------------------
+// Compute a predicted velocity n seconds into the future (given a known acceleration rate)
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::PredictShootTargetVelocity( float flDeltaTime, Vector *pVecTargetVel )
+{
+ *pVecTargetVel = GetShootTarget()->GetSmoothedVelocity();
+
+ // Unless there's a big angular velocity, we can assume he accelerates
+ // along the forward direction. Predict acceleration for
+ Vector vecForward;
+ GetShootTarget()->GetVectors( &vecForward, NULL, NULL );
+
+// float flBlendFactor = 1.0f;
+// VectorMA( *pVecTargetVel, flBlendFactor * VEHICLE_PREDICT_ACCELERATION, vecForward, *pVecTargetVel );
+// if ( pVecTargetVel->LengthSqr() > (VEHICLE_PREDICT_MAX_SPEED * VEHICLE_PREDICT_MAX_SPEED) )
+// {
+// VectorNormalize( *pVecTargetVel );
+// *pVecTargetVel *= VEHICLE_PREDICT_MAX_SPEED;
+// }
+}
+
+
+//-----------------------------------------------------------------------------
+// How many shots will I fire in a particular amount of time?
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::CountShotsInTime( float flDeltaTime ) const
+{
+ return (int)(flDeltaTime / GetActiveWeapon()->GetFireRate() + 0.5f);
+}
+
+float CNPC_MetroPolice::GetTimeForShots( int nShotCount ) const
+{
+ return nShotCount * GetActiveWeapon()->GetFireRate();
+}
+
+
+//-----------------------------------------------------------------------------
+// Visualize stitch
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::VisualizeStitch( const Vector &vecStart, const Vector &vecEnd )
+{
+ NDebugOverlay::Cross3D( vecStart, -Vector(32,32,32), Vector(32,32,32), 255, 0, 0, false, 5.0f );
+ NDebugOverlay::Cross3D( vecEnd, -Vector(32,32,32), Vector(32,32,32), 0, 255, 0, false, 5.0f );
+ NDebugOverlay::Line( vecStart, vecEnd, 0, 255, 0, true, 5.0f );
+}
+
+
+//-----------------------------------------------------------------------------
+// Visualize line of death
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::VisualizeLineOfDeath( )
+{
+ Vector vecAcross, vecStart;
+ CrossProduct( m_vecBurstLineOfDeathDelta, Vector( 0, 0, 1 ), vecAcross );
+ VectorNormalize( vecAcross );
+ NDebugOverlay::Line( m_vecBurstLineOfDeathOrigin, m_vecBurstLineOfDeathOrigin + m_vecBurstLineOfDeathDelta, 255, 255, 0, false, 5.0f );
+ VectorMA( m_vecBurstLineOfDeathOrigin, m_flBurstSteerDistance, vecAcross, vecStart );
+ NDebugOverlay::Line( vecStart, vecStart + m_vecBurstLineOfDeathDelta, 255, 0, 0, false, 5.0f );
+ VectorMA( m_vecBurstLineOfDeathOrigin, -m_flBurstSteerDistance, vecAcross, vecStart );
+ NDebugOverlay::Line( vecStart, vecStart + m_vecBurstLineOfDeathDelta, 255, 0, 0, false, 5.0f );
+}
+
+
+//-----------------------------------------------------------------------------
+// Burst mode!
+//-----------------------------------------------------------------------------
+#define AIM_AT_NEAR_DISTANCE_MIN 400.0f
+#define AIM_AT_NEAR_DISTANCE_MAX 1000.0f
+#define AIM_AT_NEAR_DISTANCE_DELTA (AIM_AT_NEAR_DISTANCE_MAX - AIM_AT_NEAR_DISTANCE_MIN)
+#define AIM_AT_NEAR_DISTANCE_BONUS -200.0f
+
+#define AIM_AT_FAR_DISTANCE_MIN 2000.0f
+#define AIM_AT_FAR_DISTANCE_BONUS_DISTANCE 500.0f
+#define AIM_AT_FAR_DISTANCE_BONUS 200.0f // Add this much bonus after each BONUS_DISTANCE
+
+
+//-----------------------------------------------------------------------------
+// Modify the stitch length
+//-----------------------------------------------------------------------------
+float CNPC_MetroPolice::ComputeDistanceStitchModifier( float flDistanceToTarget ) const
+{
+ if ( flDistanceToTarget < AIM_AT_NEAR_DISTANCE_MIN )
+ {
+ return AIM_AT_NEAR_DISTANCE_BONUS;
+ }
+
+ if ( flDistanceToTarget < AIM_AT_NEAR_DISTANCE_MAX )
+ {
+ float flFraction = 1.0f - ((flDistanceToTarget - AIM_AT_NEAR_DISTANCE_MIN) / AIM_AT_NEAR_DISTANCE_DELTA);
+ return flFraction * AIM_AT_NEAR_DISTANCE_BONUS;
+ }
+
+ if ( flDistanceToTarget > AIM_AT_FAR_DISTANCE_MIN )
+ {
+ float flFactor = (flDistanceToTarget - AIM_AT_FAR_DISTANCE_MIN) / AIM_AT_FAR_DISTANCE_BONUS_DISTANCE;
+ return flFactor * AIM_AT_FAR_DISTANCE_BONUS;
+ }
+
+ return 0.0f;
+}
+
+
+//-----------------------------------------------------------------------------
+// Set up the shot regulator
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SetupBurstShotRegulator( float flReactionTime )
+{
+ // We want a certain amount of reaction time before the shots hit the boat
+ int nDesiredShotCount = CountShotsInTime( flReactionTime );
+ GetShotRegulator()->SetBurstShotCountRange( nDesiredShotCount, nDesiredShotCount );
+ GetShotRegulator()->SetRestInterval( 0.7f, 0.9f );
+ GetShotRegulator()->Reset( true );
+ int nShots = GetShotRegulator()->GetBurstShotsRemaining();
+ OnRangeAttack1();
+ return nShots;
+}
+
+
+//-----------------------------------------------------------------------------
+// Shoots a burst right at the player
+//-----------------------------------------------------------------------------
+#define TIGHT_GROUP_MIN_DIST 750.0f
+#define TIGHT_GROUP_MIN_SPEED 400.0f
+
+void CNPC_MetroPolice::AimBurstTightGrouping( float flShotTime )
+{
+ if ( !IsCurrentlyFiringBurst() )
+ return;
+
+ // We want a certain amount of reaction time before the shots hit the boat
+ SetupBurstShotRegulator( flShotTime );
+
+ // Max number of times we can hit the enemy.
+ // Can hit more if we're slow + close
+ float flDistToTargetSqr = GetShootTarget()->WorldSpaceCenter().DistToSqr( Weapon_ShootPosition() );
+
+ int nHitCount = sk_metropolice_stitch_tight_hitcount.GetInt();
+
+ Vector vecTargetVel;
+ GetShootTarget()->GetVelocity( &vecTargetVel, NULL );
+ if (( flDistToTargetSqr > TIGHT_GROUP_MIN_DIST*TIGHT_GROUP_MIN_DIST ) ||
+ ( vecTargetVel.LengthSqr() > TIGHT_GROUP_MIN_SPEED * TIGHT_GROUP_MIN_SPEED ))
+ {
+ m_nMaxBurstHits = random->RandomInt( nHitCount, nHitCount + 1 );
+ }
+ else
+ {
+ m_nMaxBurstHits = random->RandomInt( 2 * nHitCount - 1, 2 * nHitCount + 1 );
+ }
+
+ m_nBurstMode = BURST_TIGHT_GROUPING;
+
+ // This helps the NPC model aim at the correct point
+ m_nBurstSteerMode = BURST_STEER_EXACTLY_TOWARD_TARGET;
+ m_vecBurstTargetPos = GetEnemy()->WorldSpaceCenter();
+}
+
+
+//-----------------------------------------------------------------------------
+// Reaction time for stitch
+//-----------------------------------------------------------------------------
+#define AIM_AT_TIME_DELTA_SPEED 100.0f
+#define AIM_AT_TIME_DELTA_DIST 500.0f
+#define AIM_AT_TIME_SPEED_COUNT 6
+#define AIM_AT_TIME_DIST_COUNT 7
+
+static float s_pReactionFraction[AIM_AT_TIME_DIST_COUNT][AIM_AT_TIME_SPEED_COUNT] =
+{
+ { 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 1.0f },
+ { 0.5f, 0.5f, 0.5f, 0.5f, 0.75f, 1.0f },
+ { 0.5f, 0.5f, 0.5f, 0.65f, 0.8f, 1.0f },
+ { 0.5f, 0.5f, 0.5f, 0.75f, 1.0f, 1.0f },
+ { 0.5f, 0.5f, 0.75f, 1.0f, 1.0f, 1.0f },
+ { 0.75f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f },
+ { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f },
+};
+
+float CNPC_MetroPolice::AimBurstAtReactionTime( float flReactionTime, float flDistToTarget, float flCurrentSpeed )
+{
+ flReactionTime *= sk_metropolice_stitch_reaction.GetFloat();
+
+ if ( IsEnemyInAnAirboat() )
+ {
+ float u = flCurrentSpeed / AIM_AT_TIME_DELTA_SPEED;
+ float v = flDistToTarget / AIM_AT_TIME_DELTA_DIST;
+ int nu = (int)u;
+ int nv = (int)v;
+ if (( nu < AIM_AT_TIME_SPEED_COUNT - 1 ) && ( nv < AIM_AT_TIME_DIST_COUNT - 1 ))
+ {
+ float fu = u - nu;
+ float fv = v - nv;
+ float flReactionFactor = s_pReactionFraction[nv][nu] * (1.0f - fu) * (1.0f - fv);
+ flReactionFactor += s_pReactionFraction[nv+1][nu] * (1.0f - fu) * fv;
+ flReactionFactor += s_pReactionFraction[nv][nu+1] * fu * (1.0f - fv);
+ flReactionFactor += s_pReactionFraction[nv+1][nu+1] * fu * fv;
+
+ flReactionTime *= flReactionFactor;
+ }
+ }
+
+ return flReactionTime;
+}
+
+
+//-----------------------------------------------------------------------------
+// Burst mode!
+//-----------------------------------------------------------------------------
+#define AIM_AT_SHOT_DELTA_SPEED 100.0f
+#define AIM_AT_SHOT_DELTA_DIST 500.0f
+#define AIM_AT_SHOT_SPEED_COUNT 6
+#define AIM_AT_SHOT_DIST_COUNT 6
+
+static int s_pShotCountFraction[AIM_AT_TIME_DIST_COUNT][AIM_AT_TIME_SPEED_COUNT] =
+{
+ { 3.0f, 3.0f, 2.5f, 1.5f, 1.0f, 0.0f },
+ { 3.0f, 3.0f, 2.5f, 1.25f, 0.5f, 0.0f },
+ { 2.5f, 2.5f, 2.0f, 1.0f, 0.0f, 0.0f },
+ { 2.0f, 2.0f, 1.5f, 0.5f, 0.0f, 0.0f },
+ { 1.0f, 1.0f, 1.0f, 0.5f, 0.0f, 0.0f },
+ { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f },
+};
+
+int CNPC_MetroPolice::AimBurstAtSetupHitCount( float flDistToTarget, float flCurrentSpeed )
+{
+ // Max number of times we can hit the enemy
+ int nHitCount = sk_metropolice_stitch_at_hitcount.GetInt();
+ m_nMaxBurstHits = random->RandomInt( nHitCount, nHitCount + 1 );
+
+ if ( IsEnemyInAnAirboat() )
+ {
+ float u = flCurrentSpeed / AIM_AT_SHOT_DELTA_SPEED;
+ float v = flDistToTarget / AIM_AT_SHOT_DELTA_DIST;
+ int nu = (int)u;
+ int nv = (int)v;
+ if (( nu < AIM_AT_SHOT_SPEED_COUNT - 1 ) && ( nv < AIM_AT_SHOT_DIST_COUNT - 1 ))
+ {
+ float fu = u - nu;
+ float fv = v - nv;
+ float flShotFactor = s_pShotCountFraction[nv][nu] * (1.0f - fu) * (1.0f - fv);
+ flShotFactor += s_pShotCountFraction[nv+1][nu] * (1.0f - fu) * fv;
+ flShotFactor += s_pShotCountFraction[nv][nu+1] * fu * (1.0f - fv);
+ flShotFactor += s_pShotCountFraction[nv+1][nu+1] * fu * fv;
+
+ int nExtraShots = nHitCount * flShotFactor;
+ m_nMaxBurstHits += random->RandomInt( nExtraShots, nExtraShots + 1 );
+ return nExtraShots;
+ }
+ }
+
+ return 0;
+}
+
+
+//-----------------------------------------------------------------------------
+// Burst mode!
+//-----------------------------------------------------------------------------
+#define AIM_AT_DEFAULT_STITCH_SHOT_DIST 40.0f
+#define AIM_AT_SPEED_BONUS 200.0f
+#define AIM_AT_REACTION_TIME_FRACTION 0.8f
+#define AIM_AT_NEAR_REACTION_TIME_FRACTION 0.3f
+#define AIM_AT_STEER_DISTANCE 125.0f
+
+void CNPC_MetroPolice::AimBurstAtEnemy( float flReactionTime )
+{
+ if ( !IsCurrentlyFiringBurst() )
+ return;
+
+ Vector vecVelocity;
+ GetShootTarget()->GetVelocity( &vecVelocity, NULL );
+ float flCurrentSpeed = vecVelocity.Length();
+ float flDistToTargetSqr = GetShootTarget()->WorldSpaceCenter().AsVector2D().DistToSqr( Weapon_ShootPosition().AsVector2D() );
+ float flDistToTarget = sqrt(flDistToTargetSqr);
+
+ flReactionTime = AimBurstAtReactionTime( flReactionTime, flDistToTarget, flCurrentSpeed );
+
+ // We want a certain amount of reaction time before the shots hit the boat
+ int nShotCount = SetupBurstShotRegulator( flReactionTime );
+
+ bool bIsInVehicle = IsEnemyInAnAirboat();
+ if ( bIsInVehicle )
+ {
+ m_nBurstMode = BURST_LOCK_ON_AFTER_HIT;
+ m_flBurstSteerDistance = AIM_AT_STEER_DISTANCE;
+ }
+ else
+ {
+ m_nBurstMode = BURST_ACTIVE;
+ m_flBurstSteerDistance = 0;
+ }
+ m_nBurstSteerMode = BURST_STEER_WITHIN_LINE_OF_DEATH;
+
+ // Max number of times we can hit the enemy
+ int nExtraShots = AimBurstAtSetupHitCount( flDistToTarget, flCurrentSpeed );
+ float flExtraTime = GetTimeForShots( nExtraShots ) + (1.0f - AIM_AT_REACTION_TIME_FRACTION) * flReactionTime;
+ float flReactionFraction = 1.0f - flExtraTime / flReactionTime;
+ if ( flReactionFraction < 0.5f )
+ {
+ flReactionFraction = 0.5f;
+ }
+
+ float flFirstHitTime = flReactionTime * flReactionFraction;
+ Vector vecShootAt, vecShootAtVel;
+ PredictShootTargetPosition( flFirstHitTime, 0.0f, 0.0f, &vecShootAt, &vecShootAtVel );
+
+ Vector vecDelta;
+ VectorSubtract( vecShootAt, Weapon_ShootPosition(), vecDelta );
+ float flDistanceToTarget = vecDelta.Length();
+
+ // Always stitch horizontally...
+ vecDelta.z = 0.0f;
+
+ // The max stitch distance here is used to guarantee the cop doesn't try to lead
+ // the airboat so much that he ends up shooting behind himself
+ float flMaxStitchDistance = VectorNormalize( vecDelta );
+ flMaxStitchDistance -= 50.0f;
+ if ( flMaxStitchDistance < 0 )
+ {
+ flMaxStitchDistance = 0.0f;
+ }
+
+ float flStitchLength = nShotCount * AIM_AT_DEFAULT_STITCH_SHOT_DIST;
+
+ // Modify the stitch length based on distance from the shooter
+ flStitchLength += ComputeDistanceStitchModifier( flDistanceToTarget );
+
+ if ( bIsInVehicle )
+ {
+ // Make longer stitches if the enemy is going faster
+ Vector vecEnemyVelocity = GetShootTarget()->GetSmoothedVelocity();
+ if( (GetShootTarget()->GetFlags() & FL_ONGROUND) == 0 )
+ {
+ vecEnemyVelocity.z = 0.0f;
+ }
+
+ float flEnemySpeed = VectorNormalize( vecEnemyVelocity );
+ flStitchLength += AIM_AT_SPEED_BONUS * ( flEnemySpeed / 100.0f );
+
+ // Add in a little randomness across the direction of motion...
+ // Always put it on the side we're currently looking at
+ Vector vecAcross;
+ CrossProduct( vecEnemyVelocity, Vector( 0, 0, 1 ), vecAcross );
+ VectorNormalize( vecAcross );
+
+ Vector eyeForward;
+ AngleVectors( GetEnemy()->EyeAngles(), &eyeForward );
+ if ( DotProduct( vecAcross, eyeForward ) < 0.0f )
+ {
+ vecAcross *= -1.0f;
+ }
+
+ float flMinAdd = RemapVal( flEnemySpeed, 0.0f, 200.0f, 70.0f, 30.0f );
+ VectorMA( vecShootAt, random->RandomFloat( flMinAdd, 100.0f ), vecAcross, vecShootAt );
+ }
+
+ // Compute the distance along the stitch direction to the cop. we don't want to cross that line
+ Vector vecStitchStart, vecStitchEnd;
+ VectorMA( vecShootAt, -MIN( flStitchLength * flReactionFraction, flMaxStitchDistance ), vecDelta, vecStitchStart );
+ VectorMA( vecShootAt, flStitchLength * (1.0f - flReactionFraction), vecDelta, vecStitchEnd );
+
+ // Trace down a bit to hit the ground if we're above the ground...
+ trace_t trace;
+ UTIL_TraceLine( vecStitchStart, vecStitchStart + Vector( 0, 0, -512 ), (MASK_SOLID_BRUSHONLY | MASK_WATER), NULL, COLLISION_GROUP_NONE, &trace );
+ m_vecBurstTargetPos = trace.endpos;
+ VectorSubtract( vecStitchEnd, m_vecBurstTargetPos, m_vecBurstDelta );
+
+ m_vecBurstLineOfDeathOrigin = m_vecBurstTargetPos;
+ m_vecBurstLineOfDeathDelta = m_vecBurstDelta;
+
+ m_vecBurstDelta /= (nShotCount - 1);
+
+// VisualizeStitch( m_vecBurstTargetPos, vecStitchEnd );
+// VisualizeLineOfDeath();
+}
+
+
+//-----------------------------------------------------------------------------
+// Burst mode!
+//-----------------------------------------------------------------------------
+#define AIM_IN_FRONT_OF_DEFAULT_STITCH_LENGTH 1000.0f
+#define AIM_IN_FRONT_OF_MINIMUM_DISTANCE 500.0f
+#define AIM_IN_FRONT_DRAW_LINE_OF_DEATH_FRACTION 0.5f
+#define AIM_IN_FRONT_STEER_DISTANCE 150.0f
+#define AIM_IN_FRONT_REACTION_FRACTION 0.8f
+#define AIM_IN_FRONT_EXTRA_VEL 200.0f
+
+void CNPC_MetroPolice::AimBurstInFrontOfEnemy( float flReactionTime )
+{
+ if ( !IsCurrentlyFiringBurst() )
+ return;
+
+ flReactionTime *= sk_metropolice_stitch_reaction.GetFloat();
+
+ // We want a certain amount of reaction time before the shots hit the boat
+ int nShotCount = SetupBurstShotRegulator( flReactionTime );
+
+ // Max number of times we can hit the player in the airboat
+ m_nMaxBurstHits = random->RandomInt( 3, 4 );
+ m_nBurstMode = BURST_LOCK_ON_AFTER_HIT;
+ m_nBurstSteerMode = BURST_STEER_WITHIN_LINE_OF_DEATH;
+
+ // The goal here is to slow him down. Choose a target position such that we predict
+ // where he'd be in he accelerated by N over the reaction time. Prevent him from getting there.
+ Vector vecShootAt, vecShootAtVel, vecAcross;
+ PredictShootTargetPosition( flReactionTime * AIM_IN_FRONT_REACTION_FRACTION,
+ AIM_IN_FRONT_OF_MINIMUM_DISTANCE, 0.0f, &vecShootAt, &vecShootAtVel );
+
+ // Now add in some extra vel in a random direction + try to prevent that....
+ Vector vecTargetToGun, vecExtraDistance;
+ VectorSubtract( Weapon_ShootPosition(), vecShootAt, vecTargetToGun );
+ VectorNormalize( vecTargetToGun );
+ VectorNormalize( vecShootAtVel );
+ RandomDirectionBetweenVectors( vecShootAtVel, vecTargetToGun, &vecExtraDistance );
+ vecExtraDistance *= AIM_IN_FRONT_EXTRA_VEL;
+ vecShootAt += vecExtraDistance;
+
+ CrossProduct( vecExtraDistance, Vector( 0, 0, 1 ), vecAcross );
+ VectorNormalize( vecAcross );
+
+ float flStitchLength = AIM_IN_FRONT_OF_DEFAULT_STITCH_LENGTH;
+
+ Vector vecEndPoint1, vecEndPoint2;
+ VectorSubtract( Weapon_ShootPosition(), StitchAimTarget( GetAbsOrigin(), false ), vecTargetToGun );
+ float flSign = ( DotProduct( vecAcross, vecTargetToGun ) >= 0.0f ) ? 1.0f : -1.0f;
+ VectorMA( vecShootAt, flSign * flStitchLength * AIM_IN_FRONT_REACTION_FRACTION, vecAcross, vecEndPoint1 );
+ VectorMA( vecShootAt, -flSign * flStitchLength * (1.0f - AIM_IN_FRONT_REACTION_FRACTION), vecAcross, vecEndPoint2 );
+
+ m_vecBurstTargetPos = vecEndPoint1;
+ VectorSubtract( vecEndPoint2, vecEndPoint1, m_vecBurstDelta );
+
+ // This defines the line of death, which, when crossed, results in damage
+ m_vecBurstLineOfDeathOrigin = m_vecBurstTargetPos;
+ m_vecBurstLineOfDeathDelta = m_vecBurstDelta;
+ m_flBurstSteerDistance = AIM_IN_FRONT_STEER_DISTANCE;
+
+ // Make the visual representation of the line of death lie closest to the boat.
+ VectorMA( m_vecBurstTargetPos, -AIM_IN_FRONT_STEER_DISTANCE, vecShootAtVel, m_vecBurstTargetPos );
+ m_vecBurstDelta /= (nShotCount - 1);
+
+// VisualizeStitch( m_vecBurstTargetPos, m_vecBurstTargetPos + m_vecBurstDelta * (nShotCount - 1) );
+// VisualizeLineOfDeath();
+}
+
+
+//-----------------------------------------------------------------------------
+// Aim burst behind enemy
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::AimBurstBehindEnemy( float flShotTime )
+{
+ if ( !IsCurrentlyFiringBurst() )
+ return;
+
+ flShotTime *= sk_metropolice_stitch_reaction.GetFloat();
+
+ // We want a certain amount of reaction time before the shots hit the boat
+ int nShotCount = SetupBurstShotRegulator( flShotTime );
+
+ // Max number of times we can hit the player in the airboat
+ int nHitCount = sk_metropolice_stitch_behind_hitcount.GetInt();
+ m_nMaxBurstHits = random->RandomInt( nHitCount, nHitCount + 1 );
+ m_nBurstMode = BURST_LOCK_ON_AFTER_HIT;
+ m_nBurstSteerMode = BURST_STEER_WITHIN_LINE_OF_DEATH;
+
+ // Shoot across the enemy in between the enemy and me
+ Vector vecShootAt, vecShootAtVel, vecAcross;
+ PredictShootTargetPosition( 0.0f, 0.0f, 0.0f, &vecShootAt, &vecShootAtVel );
+
+ // Choose a point in between the shooter + the target
+ Vector vecDelta;
+ VectorSubtract( Weapon_ShootPosition(), vecShootAt, vecDelta );
+ vecDelta.z = 0.0f;
+ float flDistTo = VectorNormalize( vecDelta );
+ if ( flDistTo > AIM_BEHIND_MINIMUM_DISTANCE )
+ {
+ flDistTo = AIM_BEHIND_MINIMUM_DISTANCE;
+ }
+ VectorMA( vecShootAt, flDistTo, vecDelta, vecShootAt );
+ CrossProduct( vecDelta, Vector( 0, 0, 1 ), vecAcross );
+
+ float flStitchLength = AIM_BEHIND_DEFAULT_STITCH_LENGTH;
+
+ Vector vecEndPoint1, vecEndPoint2;
+ VectorMA( vecShootAt, -flStitchLength * 0.5f, vecAcross, vecEndPoint1 );
+ VectorMA( vecShootAt, flStitchLength * 0.5f, vecAcross, vecEndPoint2 );
+
+ m_vecBurstTargetPos = vecEndPoint1;
+ VectorSubtract( vecEndPoint2, vecEndPoint1, m_vecBurstDelta );
+
+ // This defines the line of death, which, when crossed, results in damage
+ m_vecBurstLineOfDeathOrigin = m_vecBurstTargetPos;
+ m_vecBurstLineOfDeathDelta = m_vecBurstDelta;
+ m_flBurstSteerDistance = AIM_BEHIND_STEER_DISTANCE;
+
+ // Make the visual representation of the line of death lie closest to the boat.
+ VectorMA( m_vecBurstTargetPos, -AIM_BEHIND_STEER_DISTANCE, vecDelta, m_vecBurstTargetPos );
+ m_vecBurstDelta /= (nShotCount - 1);
+
+// VisualizeStitch( m_vecBurstTargetPos, m_vecBurstTargetPos + m_vecBurstDelta * (nShotCount - 1) );
+// VisualizeLineOfDeath();
+}
+
+
+//-----------------------------------------------------------------------------
+// Burst mode!
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::AimBurstAlongSideOfEnemy( float flFollowTime )
+{
+ if ( !IsCurrentlyFiringBurst() )
+ return;
+
+ flFollowTime *= sk_metropolice_stitch_reaction.GetFloat();
+
+ // We want a certain amount of reaction time before the shots hit the boat
+ int nShotCount = SetupBurstShotRegulator( flFollowTime );
+
+ // Max number of times we can hit the player in the airboat
+ int nHitCount = sk_metropolice_stitch_along_hitcount.GetInt();
+ m_nMaxBurstHits = random->RandomInt( nHitCount, nHitCount + 1 );
+ m_nBurstMode = BURST_LOCK_ON_AFTER_HIT;
+ m_nBurstSteerMode = BURST_STEER_WITHIN_LINE_OF_DEATH;
+
+ Vector vecShootAt, vecShootAtVel, vecAcross;
+ PredictShootTargetPosition( AIM_ALONG_SIDE_LINE_OF_DEATH_LEAD_TIME, 225.0f, 0.0f, &vecShootAt, &vecShootAtVel );
+ CrossProduct( vecShootAtVel, Vector( 0, 0, 1 ), vecAcross );
+ VectorNormalize( vecAcross );
+
+ // Choose the side of the vehicle which is closer to the shooter
+ Vector vecSidePoint;
+ Vector vecTargetToGun;
+ VectorSubtract( Weapon_ShootPosition(), vecShootAt, vecTargetToGun );
+ float flSign = ( DotProduct( vecTargetToGun, vecAcross ) > 0.0f ) ? 1.0f : -1.0f;
+ float flDist = AIM_ALONG_SIDE_LINE_OF_DEATH_DISTANCE + random->RandomFloat( 0.0f, 50.0f );
+ VectorMA( vecShootAt, flSign * flDist, vecAcross, vecSidePoint );
+
+ vecShootAtVel.z = 0.0f;
+ float flTargetSpeed = VectorNormalize( vecShootAtVel );
+ float flStitchLength = MAX( AIM_IN_FRONT_OF_DEFAULT_STITCH_LENGTH, flTargetSpeed * flFollowTime * 0.9 );
+
+ // This defines the line of death, which, when crossed, results in damage
+ m_vecBurstLineOfDeathOrigin = vecSidePoint;
+ VectorMultiply( vecShootAtVel, flStitchLength, m_vecBurstLineOfDeathDelta );
+
+ // Pull the endpoint a little toward the NPC firing it...
+ float flExtraDist = random->RandomFloat( 25.0f, 50.0f );
+ VectorNormalize( vecTargetToGun );
+ if ( flSign * DotProduct( vecTargetToGun, vecShootAtVel ) < 0.1f )
+ {
+ flExtraDist += 100.0f;
+ }
+ VectorMA( m_vecBurstLineOfDeathDelta, flSign * flExtraDist, vecAcross, m_vecBurstLineOfDeathDelta );
+
+ m_flBurstSteerDistance = AIM_ALONG_SIDE_STEER_DISTANCE;
+ m_vecBurstDelta = m_vecBurstLineOfDeathDelta;
+ m_vecBurstTargetPos = m_vecBurstLineOfDeathOrigin;
+
+ // Make the visual representation of the line of death lie closest to the boat.
+ VectorMA( m_vecBurstTargetPos, -flSign * AIM_ALONG_SIDE_STEER_DISTANCE, vecAcross, m_vecBurstTargetPos );
+ m_vecBurstDelta /= (nShotCount - 1);
+
+// VisualizeStitch( m_vecBurstTargetPos, m_vecBurstTargetPos + m_vecBurstDelta * (nShotCount - 1) );
+// VisualizeLineOfDeath();
+}
+
+
+//-----------------------------------------------------------------------------
+// Different burst steering modes
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::SteerBurstTowardTargetUseSpeedOnly( const Vector &vecShootAt,
+ const Vector &vecShootAtVelocity, float flPredictTime, int nShotsTillPredict )
+{
+ // Only account for changes in *speed*; ignore all changes in velocity direction, etc.
+ // This one only hits the player if there is *no* steering, just acceleration or decceleration
+ Vector vecBurstDir = m_vecBurstPredictedVelocityDir;
+ float flActualSpeed = DotProduct( vecShootAtVelocity, vecBurstDir );
+
+ vecBurstDir *= (flActualSpeed - m_vecBurstPredictedSpeed) * flPredictTime;
+ vecBurstDir /= (nShotsTillPredict - 1);
+
+ m_vecBurstPredictedSpeed = flActualSpeed;
+ m_vecBurstDelta += vecBurstDir;
+}
+
+void CNPC_MetroPolice::SteerBurstTowardTargetUseVelocity( const Vector &vecShootAt, const Vector &vecShootAtVelocity, int nShotsTillPredict )
+{
+ // Only account for all velocity changes
+ // This one looks scary in that it always gets near to the player,
+ // but it never usually hits actually.
+ Vector vecBurstDir = m_vecBurstLineOfDeathDelta;
+ m_vecBurstLineOfDeathDelta = vecShootAtVelocity;
+ vecBurstDir = vecShootAtVelocity - vecBurstDir;
+ vecBurstDir /= (nShotsTillPredict - 1);
+
+ m_vecBurstDelta += vecBurstDir;
+}
+
+void CNPC_MetroPolice::SteerBurstTowardTargetUsePosition( const Vector &vecShootAt, const Vector &vecShootAtVelocity, int nShotsTillPredict )
+{
+ // Account for velocity + position changes
+ // This method *always* hits
+ VectorSubtract( vecShootAt, m_vecBurstTargetPos, m_vecBurstDelta );
+ m_vecBurstDelta /= (nShotsTillPredict - 1);
+}
+
+void CNPC_MetroPolice::SteerBurstTowardPredictedPoint( const Vector &vecShootAt, const Vector &vecShootAtVelocity, int nShotsTillPredict )
+{
+ // Account for velocity + position changes, but only within a constrained cylinder
+ Vector vecConstrainedShootPosition;
+ CalcClosestPointOnLine( vecShootAt, m_vecBurstLineOfDeathOrigin, m_vecBurstLineOfDeathOrigin + m_vecBurstLineOfDeathDelta, vecConstrainedShootPosition );
+
+ Vector vecDelta;
+ VectorSubtract( vecShootAt, vecConstrainedShootPosition, vecDelta );
+ if ( vecDelta.LengthSqr( ) <= m_flBurstSteerDistance * m_flBurstSteerDistance )
+ {
+ vecConstrainedShootPosition = vecShootAt;
+ }
+ else
+ {
+ VectorNormalize( vecDelta );
+ VectorMA( vecConstrainedShootPosition, m_flBurstSteerDistance, vecDelta, vecConstrainedShootPosition );
+ }
+
+ // This method *always* hits if the entity is within the cylinder
+ VectorSubtract( vecConstrainedShootPosition, m_vecBurstTargetPos, m_vecBurstDelta );
+ if ( nShotsTillPredict >= 2 )
+ {
+ m_vecBurstDelta /= (nShotsTillPredict - 1);
+ }
+}
+
+#define STEER_LINE_OF_DEATH_MAX_DISTANCE 250.0f
+
+void CNPC_MetroPolice::SteerBurstWithinLineOfDeath( )
+{
+ // Account for velocity + position changes, but only within a constrained cylinder
+ Vector vecShootAt;
+ vecShootAt = StitchAimTarget( GetAbsOrigin(), false );
+
+ // If the target close to the current point the shot is on,
+ // move the shot toward the point
+ Vector vecPointOnLineOfDeath;
+ CalcClosestPointOnLine( m_vecBurstTargetPos, m_vecBurstLineOfDeathOrigin, m_vecBurstLineOfDeathOrigin + m_vecBurstLineOfDeathDelta, vecPointOnLineOfDeath );
+
+ Vector vecDelta;
+ VectorSubtract( vecShootAt, vecPointOnLineOfDeath, vecDelta );
+ if ( vecDelta.LengthSqr( ) <= m_flBurstSteerDistance * m_flBurstSteerDistance )
+ {
+ VectorSubtract( vecShootAt, m_vecBurstTargetPos, m_vecBurstDelta );
+ if ( m_vecBurstDelta.LengthSqr() > (STEER_LINE_OF_DEATH_MAX_DISTANCE * STEER_LINE_OF_DEATH_MAX_DISTANCE) )
+ {
+ VectorNormalize( m_vecBurstDelta );
+ m_vecBurstDelta *= STEER_LINE_OF_DEATH_MAX_DISTANCE;
+ }
+ }
+ else
+ {
+ // Just make the burst go back and forth alont the line of death...
+ Vector vecNext = m_vecBurstTargetPos + m_vecBurstDelta;
+
+ float t;
+ CalcClosestPointOnLine( vecNext, m_vecBurstLineOfDeathOrigin, m_vecBurstLineOfDeathOrigin + m_vecBurstLineOfDeathDelta, vecPointOnLineOfDeath, &t );
+ if (( t < -0.1f ) || ( t > 1.1f ))
+ {
+ m_vecBurstDelta *= -1.0f;
+
+ // This is necessary to make it not look like a machine is firing the gun
+ Vector vecBurstDir = m_vecBurstDelta;
+ float flLength = VectorNormalize( vecBurstDir );
+ vecBurstDir *= random->RandomFloat( -flLength * 0.5f, flLength * 0.5f );
+
+ m_vecBurstTargetPos += vecBurstDir;
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Burst mode!
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::SteerBurstTowardTarget( )
+{
+ switch ( m_nBurstSteerMode )
+ {
+ case BURST_STEER_NONE:
+ return;
+
+ case BURST_STEER_EXACTLY_TOWARD_TARGET:
+ // Necessary to get the cop looking at the target
+ m_vecBurstTargetPos = GetEnemy()->WorldSpaceCenter();
+ return;
+
+ case BURST_STEER_ADJUST_FOR_SPEED_CHANGES:
+ {
+ // Predict the airboat position at the point where we were expecting to hit them
+ if ( m_flBurstPredictTime <= gpGlobals->curtime )
+ return;
+
+ float flPredictTime = m_flBurstPredictTime - gpGlobals->curtime;
+ int nShotsTillPredict = CountShotsInTime( flPredictTime );
+ if ( nShotsTillPredict <= 1 )
+ return;
+
+ Vector vecShootAt, vecShootAtVelocity;
+ PredictShootTargetPosition( flPredictTime, 0.0f, 0.0f, &vecShootAt, &vecShootAtVelocity );
+ SteerBurstTowardTargetUseSpeedOnly( vecShootAt, vecShootAtVelocity, flPredictTime, nShotsTillPredict );
+ }
+ break;
+
+ case BURST_STEER_TOWARD_PREDICTED_POINT:
+ // Don't course-correct until the predicted time
+ if ( m_flBurstPredictTime >= gpGlobals->curtime )
+ return;
+
+ // fall through!
+
+ case BURST_STEER_WITHIN_LINE_OF_DEATH:
+ break;
+ }
+
+ SteerBurstWithinLineOfDeath( );
+}
+
+
+//-----------------------------------------------------------------------------
+// Various burst trajectory methods
+//-----------------------------------------------------------------------------
+Vector CNPC_MetroPolice::ComputeBurstLockOnTrajectory( const Vector &shootOrigin )
+{
+ Vector vecTrajectory;
+ VectorSubtract( GetEnemy()->WorldSpaceCenter(), shootOrigin, vecTrajectory );
+ VectorNormalize( vecTrajectory );
+ return vecTrajectory;
+}
+
+Vector CNPC_MetroPolice::ComputeBurstDeliberatelyMissTrajectory( const Vector &shootOrigin )
+{
+ m_vecBurstTargetPos.z += 8.0f;
+
+ Vector vecTrajectory;
+ VectorSubtract( m_vecBurstTargetPos, shootOrigin, vecTrajectory );
+ VectorNormalize( vecTrajectory );
+ return vecTrajectory;
+}
+
+Vector CNPC_MetroPolice::ComputeBurstTrajectory( const Vector &shootOrigin )
+{
+ // Perform the stitch
+ Vector vecPos = m_vecBurstTargetPos;
+
+ // For players, don't let them jump over the burst.
+ CBaseEntity *pEnemy = GetEnemy();
+ bool bIsPlayerOnFoot = pEnemy && pEnemy->IsPlayer() && !IsEnemyInAnAirboat();
+ if ( bIsPlayerOnFoot )
+ {
+ Vector vecNormalizedPt;
+ pEnemy->CollisionProp()->WorldToNormalizedSpace( vecPos, &vecNormalizedPt );
+ if ( (vecNormalizedPt.x >= -0.1f) && (vecNormalizedPt.x <= 1.1f) &&
+ (vecNormalizedPt.y >= -0.1f) && (vecNormalizedPt.y <= 1.1f) &&
+ (vecNormalizedPt.z >= -0.7f) && (vecNormalizedPt.z < 1.1f) )
+ {
+ vecPos.z = pEnemy->WorldSpaceCenter().z;
+ }
+ }
+
+ vecPos -= shootOrigin;
+
+ // Add a little noise. Even though it's non-physical, it looks better
+ // to have the same amount of noise regardless of distance from the shooter
+ // Always make the noise perpendicular to the burst direction
+ float flNoise = bIsPlayerOnFoot ? 16.0f : 32.0f;
+ Vector vecNoise;
+ CrossProduct( m_vecBurstDelta, Vector( 0, 0, 1 ), vecNoise );
+ VectorNormalize( vecNoise );
+ vecNoise *= random->RandomFloat( -flNoise, flNoise );
+ vecPos += vecNoise;
+
+ VectorNormalize( vecPos );
+
+ // X360BUG: Was causing compiler crash in release, still?
+// if ( IsPC() )
+ {
+ // Allow for steering towards the target.
+ SteerBurstTowardTarget();
+ }
+
+ // Update the burst target position
+ m_vecBurstTargetPos += m_vecBurstDelta;
+
+// NDebugOverlay::Cross3D( m_vecBurstTargetPos, -Vector(32,32,32), Vector(32,32,32), 255, 0, 255, false, 1.0f );
+
+ return vecPos;
+}
+
+
+//-----------------------------------------------------------------------------
+// Deliberately aims as close as possible w/o hitting
+//-----------------------------------------------------------------------------
+Vector CNPC_MetroPolice::AimCloseToTargetButMiss( CBaseEntity *pTarget, const Vector &shootOrigin )
+{
+ Vector vecNormalizedSpace;
+ pTarget->CollisionProp()->WorldToNormalizedSpace( shootOrigin, &vecNormalizedSpace );
+ vecNormalizedSpace -= Vector( 0.5f, 0.5f, 0.5f );
+ float flDist = VectorNormalize( vecNormalizedSpace );
+ float flMinRadius = flDist * sqrt(3.0) / sqrt( flDist * flDist - 3 );
+
+ // Choose random points in a plane perpendicular to the shoot origin.
+ Vector vecRandomDir;
+ vecRandomDir.Random( -1.0f, 1.0f );
+ VectorMA( vecRandomDir, -DotProduct( vecNormalizedSpace, vecRandomDir ), vecNormalizedSpace, vecRandomDir );
+ VectorNormalize( vecRandomDir );
+ vecRandomDir *= flMinRadius;
+
+ vecRandomDir *= 0.5f;
+ vecRandomDir += Vector( 0.5f, 0.5f, 0.5f );
+
+ Vector vecBodyTarget;
+ pTarget->CollisionProp()->NormalizedToWorldSpace( vecRandomDir, &vecBodyTarget );
+ vecBodyTarget -= shootOrigin;
+ return vecBodyTarget;
+}
+
+
+//-----------------------------------------------------------------------------
+// A burst that goes right at the enemy
+//-----------------------------------------------------------------------------
+#define MIN_TIGHT_BURST_DIST 1000.0f
+#define MAX_TIGHT_BURST_DIST 2000.0f
+
+Vector CNPC_MetroPolice::ComputeTightBurstTrajectory( const Vector &shootOrigin )
+{
+ CBaseEntity *pEnemy = GetEnemy();
+ if ( !pEnemy )
+ {
+ return BaseClass::GetActualShootTrajectory( shootOrigin );
+ }
+
+ // Aim around the player...
+ if ( m_nBurstHits >= m_nMaxBurstHits )
+ {
+ return AimCloseToTargetButMiss( pEnemy, shootOrigin );
+ }
+
+ float flDist = shootOrigin.DistTo( pEnemy->WorldSpaceCenter() );
+ float flMin = -0.2f;
+ float flMax = 1.2f;
+ if ( flDist > MIN_TIGHT_BURST_DIST )
+ {
+ flDist = clamp( flDist, MIN_TIGHT_BURST_DIST, MAX_TIGHT_BURST_DIST );
+ flMin = SimpleSplineRemapVal( flDist, MIN_TIGHT_BURST_DIST, MAX_TIGHT_BURST_DIST, -0.2f, -0.7f );
+ flMax = SimpleSplineRemapVal( flDist, MIN_TIGHT_BURST_DIST, MAX_TIGHT_BURST_DIST, 1.2f, 1.7f );
+ }
+
+ // Aim randomly at the player. Since body target uses the vehicle body target,
+ // we instead are going to not use it
+ Vector vecBodyTarget;
+ pEnemy->CollisionProp()->RandomPointInBounds( Vector( flMin, flMin, flMin ), Vector( flMax, flMax, flMax * 0.75f ), &vecBodyTarget );
+ vecBodyTarget -= shootOrigin;
+ return vecBodyTarget;
+}
+
+
+//-----------------------------------------------------------------------------
+// Burst mode!
+//-----------------------------------------------------------------------------
+Vector CNPC_MetroPolice::GetActualShootTrajectory( const Vector &shootOrigin )
+{
+ switch ( m_nBurstMode )
+ {
+ case BURST_NOT_ACTIVE:
+ return BaseClass::GetActualShootTrajectory( shootOrigin );
+
+ case BURST_LOCKED_ON:
+ if ( m_nBurstHits < m_nMaxBurstHits )
+ {
+ return ComputeBurstLockOnTrajectory( shootOrigin );
+ }
+
+ // Start shooting over the head of the enemy
+ GetShootTarget()->CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 1.0f ), &m_vecBurstTargetPos );
+ m_nBurstMode = BURST_DELIBERATELY_MISS;
+ // NOTE: Fall through to BURST_DELIBERATELY_MISS!!
+
+ case BURST_DELIBERATELY_MISS:
+ return ComputeBurstDeliberatelyMissTrajectory( shootOrigin );
+
+ case BURST_LOCK_ON_AFTER_HIT:
+ // See if our target is within the bounds of the enemy
+ if ( GetShootTarget()->CollisionProp()->IsPointInBounds( m_vecBurstTargetPos ) )
+ {
+ // Now raytrace against only the world + (good for cops on bridges)
+ trace_t tr;
+ CTraceFilterWorldOnly traceFilter;
+ UTIL_TraceLine( Weapon_ShootPosition(), m_vecBurstTargetPos, MASK_SOLID, &traceFilter, &tr );
+ if ( tr.fraction == 1.0f )
+ {
+ m_nBurstMode = BURST_LOCKED_ON;
+ }
+ }
+ // NOTE: Fall through to BURST_ACTIVE!
+
+ case BURST_ACTIVE:
+ // Stitch toward the target, we haven't hit it yet
+ return ComputeBurstTrajectory( shootOrigin );
+
+ case BURST_TIGHT_GROUPING:
+ // This one goes right at the enemy
+ return ComputeTightBurstTrajectory( shootOrigin );
+ }
+
+ Assert(0);
+ return vec3_origin;
+}
+
+
+//-----------------------------------------------------------------------------
+// Burst mode!
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::FireBullets( const FireBulletsInfo_t &info )
+{
+ CBaseEntity *pEnemy = GetEnemy();
+ bool bIsPlayer = pEnemy && pEnemy->IsPlayer();
+ if ( bIsPlayer && IsCurrentlyFiringBurst() )
+ {
+ FireBulletsInfo_t actualInfo = info;
+ if ( m_nBurstHits < m_nMaxBurstHits )
+ {
+ CBasePlayer *pPlayer = assert_cast<CBasePlayer*>(pEnemy);
+
+ // This makes it so that if the player gets hit underwater,
+ // he won't take damage if his viewpoint is above water.
+ if ( !IsEnemyInAnAirboat() && ( pPlayer->GetWaterLevel() != 3 ) )
+ {
+ actualInfo.m_nFlags |= FIRE_BULLETS_DONT_HIT_UNDERWATER;
+ }
+
+ // This test is here to see if we've damaged the player
+ int nPrevHealth = pPlayer->GetHealth();
+ int nPrevArmor = pPlayer->ArmorValue();
+
+ BaseClass::FireBullets( actualInfo );
+
+ if (( pPlayer->GetHealth() < nPrevHealth ) || ( pPlayer->ArmorValue() < nPrevArmor ))
+ {
+ ++m_nBurstHits;
+ }
+ }
+ else
+ {
+ actualInfo.m_pAdditionalIgnoreEnt = pEnemy;
+ BaseClass::FireBullets( actualInfo );
+ }
+ }
+ else
+ {
+ BaseClass::FireBullets( info );
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Behaviors! Lovely behaviors
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::CreateBehaviors()
+{
+ AddBehavior( &m_RappelBehavior );
+ AddBehavior( &m_FollowBehavior );
+ AddBehavior( &m_PolicingBehavior );
+ AddBehavior( &m_ActBusyBehavior );
+ AddBehavior( &m_AssaultBehavior );
+ AddBehavior( &m_StandoffBehavior );
+ AddBehavior( &m_FuncTankBehavior );
+
+ return BaseClass::CreateBehaviors();
+}
+
+void CNPC_MetroPolice::InputEnableManhackToss( inputdata_t &inputdata )
+{
+ if ( HasSpawnFlags( SF_METROPOLICE_NO_MANHACK_DEPLOY ) )
+ {
+ RemoveSpawnFlags( SF_METROPOLICE_NO_MANHACK_DEPLOY );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &inputdata -
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::InputSetPoliceGoal( inputdata_t &inputdata )
+{
+ CBaseEntity *pGoal = gEntList.FindEntityByName( NULL, inputdata.value.String() );
+
+ if ( pGoal == NULL )
+ {
+ DevMsg( "SetPoliceGoal: %s (%s) unable to find ai_goal_police: %s\n", GetClassname(), GetDebugName(), inputdata.value.String() );
+ return;
+ }
+
+ CAI_PoliceGoal *pPoliceGoal = dynamic_cast<CAI_PoliceGoal *>(pGoal);
+
+ if ( pPoliceGoal == NULL )
+ {
+ DevMsg( "SetPoliceGoal: %s (%s)'s target %s is not an ai_goal_police entity!\n", GetClassname(), GetDebugName(), inputdata.value.String() );
+ return;
+ }
+
+ m_PolicingBehavior.Enable( pPoliceGoal );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &inputdata -
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::InputActivateBaton( inputdata_t &inputdata )
+{
+ SetBatonState( inputdata.value.Bool() );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::AlertSound( void )
+{
+ m_Sentences.Speak( "METROPOLICE_GO_ALERT" );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::DeathSound( const CTakeDamageInfo &info )
+{
+ if ( IsOnFire() )
+ return;
+
+ m_Sentences.Speak( "METROPOLICE_DIE", SENTENCE_PRIORITY_INVALID, SENTENCE_CRITERIA_ALWAYS );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: implemented by subclasses to give them an opportunity to make
+// a sound when they lose their enemy
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::LostEnemySound( void)
+{
+ // Don't announce enemies when the player isn't a criminal
+ if ( !PlayerIsCriminal() )
+ return;
+
+ if ( gpGlobals->curtime <= m_flNextLostSoundTime )
+ return;
+
+ const char *pSentence;
+ if (!(CBaseEntity*)GetEnemy() || gpGlobals->curtime - GetEnemyLastTimeSeen() > 10)
+ {
+ pSentence = "METROPOLICE_LOST_LONG";
+ }
+ else
+ {
+ pSentence = "METROPOLICE_LOST_SHORT";
+ }
+
+ if ( m_Sentences.Speak( pSentence ) >= 0 )
+ {
+ m_flNextLostSoundTime = gpGlobals->curtime + random->RandomFloat(5.0,15.0);
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: implemented by subclasses to give them an opportunity to make
+// a sound when they lose their enemy
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::FoundEnemySound( void)
+{
+ // Don't announce enemies when I'm in arrest behavior
+ if ( HasSpawnFlags( SF_METROPOLICE_ARREST_ENEMY ) )
+ return;
+
+ m_Sentences.Speak( "METROPOLICE_REFIND_ENEMY", SENTENCE_PRIORITY_HIGH );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Indicates whether or not this npc should play an idle sound now.
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::ShouldPlayIdleSound( void )
+{
+ // If someone is waiting for a response, then respond!
+ if ( ( m_NPCState == NPC_STATE_IDLE ) || ( m_NPCState == NPC_STATE_ALERT ) )
+ {
+ if ( m_nIdleChatterType >= METROPOLICE_CHATTER_RESPONSE )
+ return FOkToMakeSound();
+ }
+
+ return BaseClass::ShouldPlayIdleSound();
+}
+
+
+//-----------------------------------------------------------------------------
+// IdleSound
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::IdleSound( void )
+{
+ bool bIsCriminal = PlayerIsCriminal();
+
+ // This happens when the NPC is waiting for his buddies to respond to him
+ switch( m_nIdleChatterType )
+ {
+ case METROPOLICE_CHATTER_WAIT_FOR_RESPONSE:
+ break;
+
+ case METROPOLICE_CHATTER_ASK_QUESTION:
+ {
+ if ( m_bPlayerIsNear && !HasMemory(bits_MEMORY_PLAYER_HARASSED) )
+ {
+ if ( m_Sentences.Speak( "METROPOLICE_IDLE_HARASS_PLAYER", SENTENCE_PRIORITY_NORMAL, SENTENCE_CRITERIA_NORMAL ) >= 0 )
+ {
+ Remember( bits_MEMORY_PLAYER_HARASSED );
+ if ( GetSquad() )
+ {
+ GetSquad()->SquadRemember(bits_MEMORY_PLAYER_HARASSED);
+ }
+ }
+ return;
+ }
+
+ if ( !random->RandomInt(0,1) )
+ break;
+
+ int nQuestionType = random->RandomInt( 0, METROPOLICE_CHATTER_RESPONSE_TYPE_COUNT );
+ if ( !IsInSquad() || ( nQuestionType == METROPOLICE_CHATTER_RESPONSE_TYPE_COUNT ) )
+ {
+ m_Sentences.Speak( bIsCriminal ? "METROPOLICE_IDLE_CR" : "METROPOLICE_IDLE" );
+ break;
+ }
+
+ static const char *pQuestion[2][METROPOLICE_CHATTER_RESPONSE_TYPE_COUNT] =
+ {
+ { "METROPOLICE_IDLE_CHECK", "METROPOLICE_IDLE_QUEST" },
+ { "METROPOLICE_IDLE_CHECK_CR", "METROPOLICE_IDLE_QUEST_CR" },
+ };
+
+ if ( m_Sentences.Speak( pQuestion[bIsCriminal][nQuestionType] ) >= 0 )
+ {
+ GetSquad()->BroadcastInteraction( g_interactionMetrocopIdleChatter, (void*)(METROPOLICE_CHATTER_RESPONSE + nQuestionType), this );
+ m_nIdleChatterType = METROPOLICE_CHATTER_WAIT_FOR_RESPONSE;
+ }
+ }
+ break;
+
+ default:
+ {
+ int nResponseType = m_nIdleChatterType - METROPOLICE_CHATTER_RESPONSE;
+
+ static const char *pResponse[2][METROPOLICE_CHATTER_RESPONSE_TYPE_COUNT] =
+ {
+ { "METROPOLICE_IDLE_CLEAR", "METROPOLICE_IDLE_ANSWER" },
+ { "METROPOLICE_IDLE_CLEAR_CR", "METROPOLICE_IDLE_ANSWER_CR" },
+ };
+
+ if ( m_Sentences.Speak( pResponse[bIsCriminal][nResponseType] ) >= 0 )
+ {
+ GetSquad()->BroadcastInteraction( g_interactionMetrocopIdleChatter, (void*)(METROPOLICE_CHATTER_ASK_QUESTION), this );
+ m_nIdleChatterType = METROPOLICE_CHATTER_ASK_QUESTION;
+ }
+ }
+ break;
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::PainSound( const CTakeDamageInfo &info )
+{
+ if ( gpGlobals->curtime < m_flNextPainSoundTime )
+ return;
+
+ // Don't make pain sounds if I'm on fire. The looping sound will take care of that for us.
+ if ( IsOnFire() )
+ return;
+
+ float healthRatio = (float)GetHealth() / (float)GetMaxHealth();
+ if ( healthRatio > 0.0f )
+ {
+ const char *pSentenceName = "METROPOLICE_PAIN";
+ if ( !HasMemory(bits_MEMORY_PAIN_HEAVY_SOUND) && (healthRatio < 0.25f) )
+ {
+ Remember( bits_MEMORY_PAIN_HEAVY_SOUND | bits_MEMORY_PAIN_LIGHT_SOUND );
+ pSentenceName = "METROPOLICE_PAIN_HEAVY";
+ }
+ else if ( !HasMemory(bits_MEMORY_PAIN_LIGHT_SOUND) && healthRatio > 0.8f )
+ {
+ Remember( bits_MEMORY_PAIN_LIGHT_SOUND );
+ pSentenceName = "METROPOLICE_PAIN_LIGHT";
+ }
+
+ // This causes it to speak it no matter what; doesn't bother with setting sounds.
+ m_Sentences.Speak( pSentenceName, SENTENCE_PRIORITY_INVALID, SENTENCE_CRITERIA_ALWAYS );
+ m_flNextPainSoundTime = gpGlobals->curtime + 1;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::GetSoundInterests( void )
+{
+ return SOUND_WORLD | SOUND_COMBAT | SOUND_PLAYER | SOUND_PLAYER_VEHICLE | SOUND_DANGER |
+ SOUND_PHYSICS_DANGER | SOUND_BULLET_IMPACT | SOUND_MOVE_AWAY;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+float CNPC_MetroPolice::MaxYawSpeed( void )
+{
+ switch( GetActivity() )
+ {
+ case ACT_TURN_LEFT:
+ case ACT_TURN_RIGHT:
+ return 120;
+
+ case ACT_RUN:
+ case ACT_RUN_HURT:
+ return 15;
+
+ case ACT_WALK:
+ case ACT_WALK_CROUCH:
+ case ACT_RUN_CROUCH:
+ return 25;
+
+ default:
+ return 45;
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//
+//
+//-----------------------------------------------------------------------------
+Class_T CNPC_MetroPolice::Classify ( void )
+{
+ return CLASS_METROPOLICE;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::PlayerIsCriminal( void )
+{
+ if ( m_PolicingBehavior.IsEnabled() && m_PolicingBehavior.TargetIsHostile() )
+ return true;
+
+ if ( GlobalEntity_GetState( "gordon_precriminal" ) == GLOBAL_ON )
+ return false;
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Overridden because if the player is a criminal, we hate them.
+// Input : pTarget - Entity with which to determine relationship.
+// Output : Returns relationship value.
+//-----------------------------------------------------------------------------
+Disposition_t CNPC_MetroPolice::IRelationType(CBaseEntity *pTarget)
+{
+ Disposition_t disp = BaseClass::IRelationType(pTarget);
+
+ if ( pTarget == NULL )
+ return disp;
+
+ // If the player's not a criminal, then we don't necessary hate him
+ if ( pTarget->Classify() == CLASS_PLAYER )
+ {
+ if ( !PlayerIsCriminal() && (disp == D_HT) )
+ {
+ // If we're pissed at the player, we're allowed to hate them.
+ if ( m_flChasePlayerTime && m_flChasePlayerTime > gpGlobals->curtime )
+ return D_HT;
+ return D_NU;
+ }
+ }
+
+ return disp;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pEvent -
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::OnAnimEventStartDeployManhack( void )
+{
+ Assert( m_iManhacks );
+
+ if ( m_iManhacks <= 0 )
+ {
+ DevMsg( "Error: Throwing manhack but out of manhacks!\n" );
+ return;
+ }
+
+ m_iManhacks--;
+
+ // Turn off the manhack on our body
+ if ( m_iManhacks <= 0 )
+ {
+ SetBodygroup( METROPOLICE_BODYGROUP_MANHACK, false );
+ }
+
+ // Create the manhack to throw
+ CNPC_Manhack *pManhack = (CNPC_Manhack *)CreateEntityByName( "npc_manhack" );
+
+ Vector vecOrigin;
+ QAngle vecAngles;
+
+ int handAttachment = LookupAttachment( "LHand" );
+ GetAttachment( handAttachment, vecOrigin, vecAngles );
+
+ pManhack->SetLocalOrigin( vecOrigin );
+ pManhack->SetLocalAngles( vecAngles );
+ pManhack->AddSpawnFlags( (SF_MANHACK_PACKED_UP|SF_MANHACK_CARRIED|SF_NPC_WAIT_FOR_SCRIPT) );
+
+ // Also fade if our parent is marked to do it
+ if ( HasSpawnFlags( SF_NPC_FADE_CORPSE ) )
+ {
+ pManhack->AddSpawnFlags( SF_NPC_FADE_CORPSE );
+ }
+
+ pManhack->Spawn();
+
+ // Make us move with his hand until we're deployed
+ pManhack->SetParent( this, handAttachment );
+
+ m_hManhack = pManhack;
+}
+
+//-----------------------------------------------------------------------------
+// Anim event handlers
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::OnAnimEventDeployManhack( animevent_t *pEvent )
+{
+ // Let it go
+ ReleaseManhack();
+
+ Vector forward, right;
+ GetVectors( &forward, &right, NULL );
+
+ IPhysicsObject *pPhysObj = m_hManhack->VPhysicsGetObject();
+
+ if ( pPhysObj )
+ {
+ Vector yawOff = right * random->RandomFloat( -1.0f, 1.0f );
+
+ Vector forceVel = ( forward + yawOff * 16.0f ) + Vector( 0, 0, 250 );
+ Vector forceAng = vec3_origin;
+
+ // Give us velocity
+ pPhysObj->AddVelocity( &forceVel, &forceAng );
+ }
+
+ // Stop dealing with this manhack
+ m_hManhack = NULL;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::OnAnimEventShove( void )
+{
+ CBaseEntity *pHurt = CheckTraceHullAttack( 16, Vector(-16,-16,-16), Vector(16,16,16), 15, DMG_CLUB, 1.0f, false );
+
+ if ( pHurt )
+ {
+ Vector vecForceDir = ( pHurt->WorldSpaceCenter() - WorldSpaceCenter() );
+
+ CBasePlayer *pPlayer = ToBasePlayer( pHurt );
+
+ if ( pPlayer != NULL )
+ {
+ //Kick the player angles
+ pPlayer->ViewPunch( QAngle( 8, 14, 0 ) );
+
+ Vector dir = pHurt->GetAbsOrigin() - GetAbsOrigin();
+ VectorNormalize(dir);
+
+ QAngle angles;
+ VectorAngles( dir, angles );
+ Vector forward, right;
+ AngleVectors( angles, &forward, &right, NULL );
+
+ //If not on ground, then don't make them fly!
+ if ( !(pHurt->GetFlags() & FL_ONGROUND ) )
+ forward.z = 0.0f;
+
+ //Push the target back
+ pHurt->ApplyAbsVelocityImpulse( forward * 250.0f );
+
+ // Force the player to drop anyting they were holding
+ pPlayer->ForceDropOfCarriedPhysObjects();
+ }
+
+ // Play a random attack hit sound
+ EmitSound( "NPC_Metropolice.Shove" );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::OnAnimEventBatonOn( void )
+{
+#ifndef HL2MP
+
+ CWeaponStunStick *pStick = dynamic_cast<CWeaponStunStick *>(GetActiveWeapon());
+
+ if ( pStick )
+ {
+ pStick->SetStunState( true );
+ }
+#endif
+
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::OnAnimEventBatonOff( void )
+{
+#ifndef HL2MP
+
+ CWeaponStunStick *pStick = dynamic_cast<CWeaponStunStick *>(GetActiveWeapon());
+
+ if ( pStick )
+ {
+ pStick->SetStunState( false );
+ }
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//
+// Input : *pEvent -
+//
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::HandleAnimEvent( animevent_t *pEvent )
+{
+ // Shove!
+ if ( pEvent->event == AE_METROPOLICE_SHOVE )
+ {
+ OnAnimEventShove();
+ return;
+ }
+
+ if ( pEvent->event == AE_METROPOLICE_BATON_ON )
+ {
+ OnAnimEventBatonOn();
+ return;
+ }
+
+ if ( pEvent->event == AE_METROPOLICE_BATON_OFF )
+ {
+ OnAnimEventBatonOff();
+ return;
+ }
+
+ if ( pEvent->event == AE_METROPOLICE_START_DEPLOY )
+ {
+ OnAnimEventStartDeployManhack();
+ return;
+ }
+
+ if ( pEvent->event == AE_METROPOLICE_DRAW_PISTOL )
+ {
+ m_fWeaponDrawn = true;
+ if( GetActiveWeapon() )
+ {
+ GetActiveWeapon()->RemoveEffects( EF_NODRAW );
+ }
+ return;
+ }
+
+ if ( pEvent->event == AE_METROPOLICE_DEPLOY_MANHACK )
+ {
+ OnAnimEventDeployManhack( pEvent );
+ return;
+ }
+
+ BaseClass::HandleAnimEvent( pEvent );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: This is a generic function (to be implemented by sub-classes) to
+// handle specific interactions between different types of characters
+// (For example the barnacle grabbing an NPC)
+// Input : Constant for the type of interaction
+// Output : true - if sub-class has a response for the interaction
+// false - if sub-class has no response
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter* sourceEnt)
+{
+ if ( interactionType == g_interactionMetrocopStartedStitch )
+ {
+ // If anybody in our squad started a stitch, we can't for a little while
+ m_flValidStitchTime = gpGlobals->curtime + random->RandomFloat( METROPOLICE_SQUAD_STITCH_MIN_INTERVAL, METROPOLICE_SQUAD_STITCH_MAX_INTERVAL );
+ return true;
+ }
+
+ if ( interactionType == g_interactionMetrocopIdleChatter )
+ {
+ m_nIdleChatterType = (int)data;
+ return true;
+ }
+
+ if ( interactionType == g_interactionMetrocopClearSentenceQueues )
+ {
+ m_Sentences.ClearQueue();
+ return true;
+ }
+
+ // React to being hit by physics objects
+ if ( interactionType == g_interactionHitByPlayerThrownPhysObj )
+ {
+ // Ignore if I'm in scripted state
+ if ( !IsInAScript() && (m_NPCState != NPC_STATE_SCRIPT) )
+ {
+ SetCondition( COND_METROPOLICE_PHYSOBJECT_ASSAULT );
+ }
+ else
+ {
+ AdministerJustice();
+ }
+
+ // See if the object is the cupcop can. If so, fire the output (for x360 achievement)
+ CBaseProp *pProp = (CBaseProp*)data;
+ if( pProp != NULL )
+ {
+ if( pProp->NameMatches("cupcop_can") )
+ m_OnCupCopped.FireOutput( this, NULL );
+ }
+
+ return true;
+ }
+
+ return BaseClass::HandleInteraction( interactionType, data, sourceEnt );
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+Activity CNPC_MetroPolice::NPC_TranslateActivity( Activity newActivity )
+{
+ if( IsOnFire() && newActivity == ACT_RUN )
+ {
+ return ACT_RUN_ON_FIRE;
+ }
+
+ // If we're shoving, see if we should be more forceful in doing so
+ if ( newActivity == ACT_PUSH_PLAYER )
+ {
+ if ( m_nNumWarnings >= METROPOLICE_MAX_WARNINGS )
+ return ACT_MELEE_ATTACK1;
+ }
+
+ newActivity = BaseClass::NPC_TranslateActivity( newActivity );
+
+ // This will put him into an angry idle, which will then be translated
+ // by the weapon to the appropriate type.
+ if ( m_fWeaponDrawn && newActivity == ACT_IDLE && ( GetState() == NPC_STATE_COMBAT || BatonActive() ) )
+ {
+ newActivity = ACT_IDLE_ANGRY;
+ }
+
+ return newActivity;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Makes the held manhack solid
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::ReleaseManhack( void )
+{
+ Assert( m_hManhack );
+
+ // Make us physical
+ m_hManhack->RemoveSpawnFlags( SF_MANHACK_CARRIED );
+ m_hManhack->CreateVPhysics();
+
+ // Release us
+ m_hManhack->RemoveSolidFlags( FSOLID_NOT_SOLID );
+ m_hManhack->SetMoveType( MOVETYPE_VPHYSICS );
+ m_hManhack->SetParent( NULL );
+
+ // Make us active
+ m_hManhack->RemoveSpawnFlags( SF_NPC_WAIT_FOR_SCRIPT );
+ m_hManhack->ClearSchedule( "Manhack released by metropolice" );
+
+ // Start him with knowledge of our current enemy
+ if ( GetEnemy() )
+ {
+ m_hManhack->SetEnemy( GetEnemy() );
+ m_hManhack->SetState( NPC_STATE_COMBAT );
+
+ m_hManhack->UpdateEnemyMemory( GetEnemy(), GetEnemy()->GetAbsOrigin() );
+ }
+
+ // Place him into our squad so we can communicate
+ if ( m_pSquad )
+ {
+ m_pSquad->AddToSquad( m_hManhack );
+ }
+}
+
+//-----------------------------------------------------------------------------
+//
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::Event_Killed( const CTakeDamageInfo &info )
+{
+ // Release the manhack if we're in the middle of deploying him
+ if ( m_hManhack && m_hManhack->IsAlive() )
+ {
+ ReleaseManhack();
+ m_hManhack = NULL;
+ }
+
+ CBasePlayer *pPlayer = ToBasePlayer( info.GetAttacker() );
+
+ if ( pPlayer != NULL )
+ {
+ CHalfLife2 *pHL2GameRules = static_cast<CHalfLife2 *>(g_pGameRules);
+
+ // Attempt to drop health
+ if ( pHL2GameRules->NPC_ShouldDropHealth( pPlayer ) )
+ {
+ DropItem( "item_healthvial", WorldSpaceCenter()+RandomVector(-4,4), RandomAngle(0,360) );
+ pHL2GameRules->NPC_DroppedHealth();
+ }
+ }
+
+ BaseClass::Event_Killed( info );
+}
+
+//-----------------------------------------------------------------------------
+// Try to enter a slot where we shoot a pistol
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::TryToEnterPistolSlot( int nSquadSlot )
+{
+ // This logic here will not allow us to occupy the a squad slot
+ // too soon after we already were in it.
+ if ( ( m_LastShootSlot != nSquadSlot || !m_TimeYieldShootSlot.Expired() ) &&
+ OccupyStrategySlot( nSquadSlot ) )
+ {
+ if ( m_LastShootSlot != nSquadSlot )
+ {
+ m_TimeYieldShootSlot.Reset();
+ m_LastShootSlot = nSquadSlot;
+ }
+ return true;
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------
+// Combat schedule selection
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectRangeAttackSchedule()
+{
+ if ( HasSpawnFlags( SF_METROPOLICE_ALWAYS_STITCH ) )
+ {
+ int nSched = SelectMoveToLedgeSchedule();
+ if ( nSched != SCHED_NONE )
+ return nSched;
+ }
+
+ // Range attack if we're able
+ if( TryToEnterPistolSlot( SQUAD_SLOT_ATTACK1 ) || TryToEnterPistolSlot( SQUAD_SLOT_ATTACK2 ))
+ return SCHED_RANGE_ATTACK1;
+
+ // We're not in a shoot slot... so we've allowed someone else to grab it
+ m_LastShootSlot = SQUAD_SLOT_NONE;
+
+ if( CanDeployManhack() && OccupyStrategySlot( SQUAD_SLOT_POLICE_DEPLOY_MANHACK ) )
+ {
+ return SCHED_METROPOLICE_DEPLOY_MANHACK;
+ }
+
+ return SCHED_METROPOLICE_ADVANCE;
+}
+
+
+//-----------------------------------------------------------------------------
+// How many squad members are trying to arrest the player?
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SquadArrestCount()
+{
+ int nCount = 0;
+
+ AISquadIter_t iter;
+ CAI_BaseNPC *pSquadmate = m_pSquad->GetFirstMember( &iter );
+ while ( pSquadmate )
+ {
+ if ( pSquadmate->IsCurSchedule( SCHED_METROPOLICE_ARREST_ENEMY ) ||
+ pSquadmate->IsCurSchedule( SCHED_METROPOLICE_WARN_AND_ARREST_ENEMY ) )
+ {
+ ++nCount;
+ }
+
+ pSquadmate = m_pSquad->GetNextMember( &iter );
+ }
+
+ return nCount;
+}
+
+
+//-----------------------------------------------------------------------------
+// Arrest schedule selection
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectScheduleArrestEnemy()
+{
+ if ( !HasSpawnFlags( SF_METROPOLICE_ARREST_ENEMY ) || !IsInSquad() )
+ return SCHED_NONE;
+
+ if ( !HasCondition( COND_SEE_ENEMY ) )
+ return SCHED_NONE;
+
+ if ( !m_fWeaponDrawn )
+ return SCHED_METROPOLICE_DRAW_PISTOL;
+
+ // First guy that sees the enemy will tell him to freeze
+ if ( OccupyStrategySlot( SQUAD_SLOT_POLICE_ARREST_ENEMY ) )
+ return SCHED_METROPOLICE_WARN_AND_ARREST_ENEMY;
+
+ // Squad members 1 -> n will simply gain a line of sight
+ return SCHED_METROPOLICE_ARREST_ENEMY;
+}
+
+
+//-----------------------------------------------------------------------------
+// Combat schedule selection
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectScheduleNewEnemy()
+{
+ int nSched = SelectScheduleArrestEnemy();
+ if ( nSched != SCHED_NONE )
+ return nSched;
+
+ if ( HasCondition( COND_NEW_ENEMY ) )
+ {
+ m_flNextLedgeCheckTime = gpGlobals->curtime;
+
+ if( CanDeployManhack() && OccupyStrategySlot( SQUAD_SLOT_POLICE_DEPLOY_MANHACK ) )
+ return SCHED_METROPOLICE_DEPLOY_MANHACK;
+ }
+
+ if ( !m_fWeaponDrawn )
+ return SCHED_METROPOLICE_DRAW_PISTOL;
+
+ // Switch our baton on, if it's not already
+ if ( HasBaton() && BatonActive() == false && IsCurSchedule( SCHED_METROPOLICE_ACTIVATE_BATON ) == false )
+ {
+ SetTarget( GetEnemy() );
+ SetBatonState( true );
+ m_flBatonDebounceTime = gpGlobals->curtime + random->RandomFloat( 2.5f, 4.0f );
+ return SCHED_METROPOLICE_ACTIVATE_BATON;
+ }
+
+ return SCHED_NONE;
+}
+
+
+//-----------------------------------------------------------------------------
+// Sound investigation
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectScheduleInvestigateSound()
+{
+ // SEE_ENEMY is set if LOS is available *and* we're looking the right way
+ // Don't investigate if the player's not a criminal.
+ if ( PlayerIsCriminal() && !HasCondition( COND_SEE_ENEMY ) )
+ {
+ if ( HasCondition( COND_HEAR_COMBAT ) || HasCondition( COND_HEAR_PLAYER ) )
+ {
+ if ( m_pSquad && OccupyStrategySlot( SQUAD_SLOT_INVESTIGATE_SOUND ) )
+ {
+ return SCHED_METROPOLICE_INVESTIGATE_SOUND;
+ }
+ }
+ }
+
+ return SCHED_NONE;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::OnObstructionPreSteer( AILocalMoveGoal_t *pMoveGoal, float distClear, AIMoveResult_t *pResult )
+{
+ if ( pMoveGoal->directTrace.pObstruction )
+ {
+ // Is it a physics prop? Store it off as the last thing to block me
+ CPhysicsProp *pProp = dynamic_cast<CPhysicsProp*>( pMoveGoal->directTrace.pObstruction );
+ if ( pProp && pProp->GetHealth() )
+ {
+ m_hBlockingProp = pProp;
+ }
+ else
+ {
+ m_hBlockingProp = NULL;
+ }
+ }
+
+ return BaseClass::OnObstructionPreSteer( pMoveGoal, distClear, pResult );
+}
+
+//-----------------------------------------------------------------------------
+// Combat schedule selection
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectScheduleNoDirectEnemy()
+{
+ // If you can't attack, but you can deploy a manhack, do it!
+ if( CanDeployManhack() && OccupyStrategySlot( SQUAD_SLOT_POLICE_DEPLOY_MANHACK ) )
+ return SCHED_METROPOLICE_DEPLOY_MANHACK;
+
+ // If you can't attack, but you have a baton & there's a physics object in front of you, swat it
+ if ( m_hBlockingProp && HasBaton() )
+ {
+ SetTarget( m_hBlockingProp );
+ m_hBlockingProp = NULL;
+ return SCHED_METROPOLICE_SMASH_PROP;
+ }
+
+ return SCHED_METROPOLICE_CHASE_ENEMY;
+}
+
+
+//-----------------------------------------------------------------------------
+// Combat schedule selection
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectCombatSchedule()
+{
+ // Announce a new enemy
+ if ( HasCondition( COND_NEW_ENEMY ) )
+ {
+ AnnounceEnemyType( GetEnemy() );
+ }
+
+ int nResult = SelectScheduleNewEnemy();
+ if ( nResult != SCHED_NONE )
+ return nResult;
+
+ if( !m_fWeaponDrawn )
+ {
+ return SCHED_METROPOLICE_DRAW_PISTOL;
+ }
+
+ if (!HasBaton() && ((float)m_nRecentDamage / (float)GetMaxHealth()) > RECENT_DAMAGE_THRESHOLD)
+ {
+ m_nRecentDamage = 0;
+ m_flRecentDamageTime = 0;
+ m_Sentences.Speak( "METROPOLICE_COVER_HEAVY_DAMAGE", SENTENCE_PRIORITY_MEDIUM, SENTENCE_CRITERIA_NORMAL );
+
+ return SCHED_TAKE_COVER_FROM_ENEMY;
+ }
+
+ if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) )
+ {
+ if ( !GetShotRegulator()->IsInRestInterval() )
+ return SelectRangeAttackSchedule();
+ else
+ return SCHED_METROPOLICE_ADVANCE;
+ }
+
+ if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) )
+ {
+ if ( m_BatonSwingTimer.Expired() )
+ {
+ // Stop chasing the player now that we've taken a swing at them
+ m_flChasePlayerTime = 0;
+ m_BatonSwingTimer.Set( 1.0, 1.75 );
+ return SCHED_MELEE_ATTACK1;
+ }
+ else
+ return SCHED_COMBAT_FACE;
+ }
+
+ if ( HasCondition( COND_TOO_CLOSE_TO_ATTACK ) )
+ {
+ return SCHED_BACK_AWAY_FROM_ENEMY;
+ }
+
+ if ( HasCondition( COND_LOW_PRIMARY_AMMO ) || HasCondition( COND_NO_PRIMARY_AMMO ) )
+ {
+ AnnounceOutOfAmmo( );
+ return SCHED_HIDE_AND_RELOAD;
+ }
+
+ if ( HasCondition(COND_WEAPON_SIGHT_OCCLUDED) && !HasBaton() )
+ {
+ // If they are hiding behind something that we can destroy, start shooting at it.
+ CBaseEntity *pBlocker = GetEnemyOccluder();
+ if ( pBlocker && pBlocker->GetHealth() > 0 && OccupyStrategySlotRange( SQUAD_SLOT_POLICE_ATTACK_OCCLUDER1, SQUAD_SLOT_POLICE_ATTACK_OCCLUDER2 ) )
+ {
+ m_Sentences.Speak( "METROPOLICE_SHOOT_COVER" );
+ return SCHED_SHOOT_ENEMY_COVER;
+ }
+ }
+
+ if (HasCondition(COND_ENEMY_OCCLUDED))
+ {
+ if ( GetEnemy() && !(GetEnemy()->GetFlags() & FL_NOTARGET) )
+ {
+ // Charge in and break the enemy's cover!
+ return SCHED_ESTABLISH_LINE_OF_FIRE;
+ }
+ }
+
+ nResult = SelectScheduleNoDirectEnemy();
+ if ( nResult != SCHED_NONE )
+ return nResult;
+
+ return SCHED_NONE;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: This is a bridge between stunstick, NPC and its behavior
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::ShouldKnockOutTarget( CBaseEntity *pTarget )
+{
+ if ( m_PolicingBehavior.IsEnabled() && m_PolicingBehavior.ShouldKnockOutTarget( pTarget ) )
+ return true;
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: This is a bridge between stunstick, NPC and its behavior
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::KnockOutTarget( CBaseEntity *pTarget )
+{
+ if ( m_PolicingBehavior.IsEnabled() )
+ {
+ m_PolicingBehavior.KnockOutTarget( pTarget );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Can me enemy see me?
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::CanEnemySeeMe( )
+{
+ if ( GetEnemy()->IsPlayer() )
+ {
+ if ( static_cast<CBasePlayer*>(GetEnemy())->FInViewCone( this ) )
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------
+// Choose weights about where we can use particular stitching behaviors
+//-----------------------------------------------------------------------------
+#define STITCH_MIN_DISTANCE 1000.0f
+#define STITCH_MIN_DISTANCE_SLOW 1250.0f
+
+#define STITCH_AT_CONE 0.866f // cos(30)
+#define STITCH_AT_CONE_WHEN_VISIBLE_MAX 0.3f // cos(?)
+#define STITCH_AT_CONE_WHEN_VISIBLE_MIN 0.707f // cos(45)
+#define STITCH_AT_COS_MIN_SPIN_ANGLE 0.2f
+
+float CNPC_MetroPolice::StitchAtWeight( float flDist, float flSpeed, float flDot, float flReactionTime, const Vector &vecTargetToGun )
+{
+ // Can't do an 'attacking' stitch if it's too soon
+ if ( m_flValidStitchTime > gpGlobals->curtime )
+ return 0.0f;
+
+ // No squad slots? no way.
+ if( IsStrategySlotRangeOccupied( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ return false;
+
+ // Don't do it if the player doesn't have enough time to react
+ if ( flDist < STITCH_MIN_DISTANCE )
+ return 0.0f;
+
+ // Don't do it if the player is farther but really slow
+ if ( ( flDist < STITCH_MIN_DISTANCE_SLOW ) && ( flSpeed < 150.0f ) )
+ return 0.0f;
+
+ // Does the predicted stitch position cross the plane from me to the target's initial position?
+ // If so, it'll look really dumb. Disallow that.
+ Vector vecGunToPredictedTarget, vecShootAtVel;
+ PredictShootTargetPosition( flReactionTime, 0.0f, 0.0f, &vecGunToPredictedTarget, &vecShootAtVel );
+ vecGunToPredictedTarget -= Weapon_ShootPosition();
+ vecGunToPredictedTarget.z = 0.0f;
+ VectorNormalize( vecGunToPredictedTarget );
+
+ Vector2D vecGunToTarget;
+ Vector2DMultiply( vecTargetToGun.AsVector2D(), -1.0f, vecGunToTarget );
+ Vector2DNormalize( vecGunToTarget );
+ if ( DotProduct2D( vecGunToTarget, vecGunToPredictedTarget.AsVector2D() ) <= STITCH_AT_COS_MIN_SPIN_ANGLE )
+ return 0.0f;
+
+ // If the cop is in the view cone, then up the cone in which the stitch will occur
+ float flConeAngle = STITCH_AT_CONE;
+ if ( CanEnemySeeMe() )
+ {
+ flDist = clamp( flDist, 1500.0f, 2500.0f );
+ flConeAngle = RemapVal( flDist, 1500.0f, 2500.0f, STITCH_AT_CONE_WHEN_VISIBLE_MIN, STITCH_AT_CONE_WHEN_VISIBLE_MAX );
+ }
+
+ flDot = clamp( flDot, -1.0f, flConeAngle );
+ return RemapVal( flDot, -1.0f, flConeAngle, 0.5f, 1.0f );
+}
+
+
+#define STITCH_ACROSS_CONE 0.5f // cos(60)
+
+float CNPC_MetroPolice::StitchAcrossWeight( float flDist, float flSpeed, float flDot, float flReactionTime )
+{
+ return 0.0f;
+
+ // Can't do an 'attacking' stitch if it's too soon
+ if ( m_flValidStitchTime > gpGlobals->curtime )
+ return 0.0f;
+
+ // No squad slots? no way.
+ if( IsStrategySlotRangeOccupied( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ return 0.0f;
+
+ if ( flDist < STITCH_MIN_DISTANCE )
+ return 0.0f;
+
+ // Don't do it if the player doesn't have enough time to react
+ if ( flDist < flSpeed * flReactionTime )
+ return 0.0f;
+
+ // We want to stitch across if we're within the stitch across cone
+ if ( flDot < STITCH_ACROSS_CONE )
+ return 0.0f;
+
+ return 1.0f;
+}
+
+
+#define STITCH_ALONG_MIN_CONE 0.866f // cos(30)
+#define STITCH_ALONG_MIN_CONE_WHEN_VISIBLE 0.707f // cos(45)
+#define STITCH_ALONG_MAX_CONE -0.4f //
+#define STITCH_ALONG_MIN_SPEED 300.0f
+
+float CNPC_MetroPolice::StitchAlongSideWeight( float flDist, float flSpeed, float flDot )
+{
+ // No squad slots? no way.
+ if( IsStrategySlotRangeOccupied( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) &&
+ IsStrategySlotRangeOccupied( SQUAD_SLOT_POLICE_COVERING_FIRE1, SQUAD_SLOT_POLICE_COVERING_FIRE2 ) )
+ return 0.0f;
+
+ if ( flDist < (AIM_ALONG_SIDE_LINE_OF_DEATH_DISTANCE + AIM_ALONG_SIDE_STEER_DISTANCE + 100.0f) )
+ return 0.0f;
+
+ if ( flSpeed < STITCH_ALONG_MIN_SPEED )
+ return 0.0f;
+
+ // We want to stitch across if we're within the stitch across cone
+ float flMinConeAngle = STITCH_ALONG_MIN_CONE;
+ bool bCanEnemySeeMe = CanEnemySeeMe( );
+ if ( bCanEnemySeeMe )
+ {
+ flMinConeAngle = STITCH_ALONG_MIN_CONE_WHEN_VISIBLE;
+ }
+
+ if (( flDot > flMinConeAngle ) || ( flDot < STITCH_ALONG_MAX_CONE ))
+ return 0.0f;
+
+ return bCanEnemySeeMe ? 1.0f : 2.0f;
+}
+
+#define STITCH_BEHIND_MIN_CONE 0.0f // cos(90)
+
+float CNPC_MetroPolice::StitchBehindWeight( float flDist, float flSpeed, float flDot )
+{
+ return 0.0f;
+
+ // No squad slots? no way.
+ if( IsStrategySlotRangeOccupied( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) &&
+ IsStrategySlotRangeOccupied( SQUAD_SLOT_POLICE_COVERING_FIRE1, SQUAD_SLOT_POLICE_COVERING_FIRE2 ) )
+ return 0.0f;
+
+ if ( flDist < AIM_BEHIND_MINIMUM_DISTANCE )
+ return 0.0f;
+
+ // We want to stitch across if we're within the stitch across cone
+ if ( flDot > STITCH_BEHIND_MIN_CONE )
+ return 0.0f;
+
+ // If we're close, reduce the chances of this if we're also slow
+ if ( flDist < STITCH_MIN_DISTANCE )
+ {
+ flSpeed = clamp( flSpeed, 300.0f, 450.0f );
+ float flWeight = RemapVal( flSpeed, 300.0f, 450.0f, 0.0f, 1.0f );
+ return flWeight;
+ }
+
+ return 1.0f;
+}
+
+float CNPC_MetroPolice::StitchTightWeight( float flDist, float flSpeed, const Vector &vecTargetToGun, const Vector &vecVelocity )
+{
+ // No squad slots? no way.
+ if( IsStrategySlotRangeOccupied( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) &&
+ IsStrategySlotRangeOccupied( SQUAD_SLOT_POLICE_COVERING_FIRE1, SQUAD_SLOT_POLICE_COVERING_FIRE2 ) )
+ return 0.0f;
+
+ if ( flDist > STITCH_MIN_DISTANCE )
+ {
+ if ( flDist > 2000.0f )
+ return 0.0f;
+
+ // We can stitch tight if they are close and no other rules apply.
+ return 0.0001f;
+ }
+
+ // If we're heading right at him, them fire it!
+ Vector vecTargetToGunDir = vecTargetToGun;
+ Vector vecVelocityDir = vecVelocity;
+ VectorNormalize( vecTargetToGunDir );
+ VectorNormalize( vecVelocityDir );
+
+ if ( DotProduct( vecTargetToGunDir, vecVelocityDir ) > 0.95f )
+ return 8.0f;
+
+ // If we're on the same level, fire at him!
+ if ( ( fabs(vecTargetToGun.z) < 50.0f ) && ( flDist < STITCH_MIN_DISTANCE ) )
+ return 1.0f;
+
+ flSpeed = clamp( flSpeed, 300.0f, 450.0f );
+ float flWeight = RemapVal( flSpeed, 300.0f, 450.0f, 1.0f, 0.0f );
+ return flWeight;
+}
+
+
+//-----------------------------------------------------------------------------
+// Combat schedule selection
+//-----------------------------------------------------------------------------
+#define STITCH_REACTION_TIME 2.0f
+#define STITCH_SCHEDULE_COUNT 5
+
+int CNPC_MetroPolice::SelectStitchSchedule()
+{
+ // If the boat is very close to us, we're going to stitch at it
+ // even if the squad slot is full..
+ Vector vecTargetToGun;
+ Vector vecTarget = StitchAimTarget( GetAbsOrigin(), false );
+ VectorSubtract( Weapon_ShootPosition(), vecTarget, vecTargetToGun );
+
+ Vector2D vecTargetToGun2D = vecTargetToGun.AsVector2D();
+ float flDist = Vector2DNormalize( vecTargetToGun2D );
+
+ if ( HasSpawnFlags( SF_METROPOLICE_NO_FAR_STITCH ) )
+ {
+ if ( flDist > 6000.0f )
+ return SCHED_NONE;
+ }
+
+ float flReactionTime = STITCH_REACTION_TIME * sk_metropolice_stitch_reaction.GetFloat();
+ Vector vecVelocity;
+ PredictShootTargetVelocity( flReactionTime, &vecVelocity );
+
+ Vector2D vecVelocity2D = vecVelocity.AsVector2D();
+ float flSpeed = Vector2DNormalize( vecVelocity2D );
+ float flDot = DotProduct2D( vecTargetToGun2D, vecVelocity2D );
+
+ float flWeight[STITCH_SCHEDULE_COUNT];
+ flWeight[0] = StitchAtWeight( flDist, flSpeed, flDot, flReactionTime, vecTargetToGun );
+ flWeight[1] = flWeight[0] + StitchAcrossWeight( flDist, flSpeed, flDot, flReactionTime );
+ flWeight[2] = flWeight[1] + StitchTightWeight( flDist, flSpeed, vecTargetToGun, vecVelocity );
+ flWeight[3] = flWeight[2] + StitchAlongSideWeight( flDist, flSpeed, flDot );
+ flWeight[4] = flWeight[3] + StitchBehindWeight( flDist, flSpeed, flDot );
+
+ if ( flWeight[STITCH_SCHEDULE_COUNT - 1] == 0.0f )
+ return SCHED_NONE;
+
+ int pSched[STITCH_SCHEDULE_COUNT] =
+ {
+ SCHED_METROPOLICE_AIM_STITCH_AT_AIRBOAT,
+ SCHED_METROPOLICE_AIM_STITCH_IN_FRONT_OF_AIRBOAT,
+ SCHED_METROPOLICE_AIM_STITCH_TIGHTLY,
+ SCHED_METROPOLICE_AIM_STITCH_ALONG_SIDE_OF_AIRBOAT,
+ SCHED_METROPOLICE_AIM_STITCH_BEHIND_AIRBOAT,
+ };
+
+ int i;
+ float flRand = random->RandomFloat( 0.0f, flWeight[STITCH_SCHEDULE_COUNT - 1] );
+ for ( i = 0; i < STITCH_SCHEDULE_COUNT; ++i )
+ {
+ if ( flRand <= flWeight[i] )
+ break;
+ }
+
+ // If we're basically a covering activity, take up that slot
+ if ( i >= 3 )
+ {
+ if( OccupyStrategySlotRange( SQUAD_SLOT_POLICE_COVERING_FIRE1, SQUAD_SLOT_POLICE_COVERING_FIRE2 ) )
+ {
+ return pSched[i];
+ }
+ }
+
+ if( OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ {
+ if ( IsInSquad() && (i < 2) )
+ {
+ GetSquad()->BroadcastInteraction( g_interactionMetrocopStartedStitch, NULL );
+ }
+
+ return pSched[i];
+ }
+
+ return SCHED_NONE;
+}
+
+
+//-----------------------------------------------------------------------------
+// Combat schedule selection
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectMoveToLedgeSchedule()
+{
+ // Prevent a bunch of unnecessary raycasts.
+ if ( m_flNextLedgeCheckTime > gpGlobals->curtime )
+ return SCHED_NONE;
+
+ // If the NPC is above the airboat (say, on a bridge), make sure he
+ // goes to the closest ledge. (may need a spawnflag for this)
+ if ( (GetAbsOrigin().z - GetShootTarget()->GetAbsOrigin().z) >= 150.0f )
+ {
+ m_flNextLedgeCheckTime = gpGlobals->curtime + 3.0f;
+
+ // We need to be able to shoot downward at a 60 degree angle.
+ Vector vecDelta;
+ VectorSubtract( GetShootTarget()->WorldSpaceCenter(), Weapon_ShootPosition(), vecDelta );
+ vecDelta.z = 0.0f;
+ VectorNormalize( vecDelta );
+
+ // At this point, vecDelta is 45 degrees below horizontal.
+ vecDelta.z = -1;
+ vecDelta *= 100.0f;
+
+ trace_t tr;
+ CTraceFilterWorldOnly traceFilter;
+ UTIL_TraceLine( Weapon_ShootPosition(), Weapon_ShootPosition() + vecDelta, MASK_SOLID, &traceFilter, &tr );
+
+ if (tr.endpos.z >= GetAbsOrigin().z - 25.0f )
+ return SCHED_METROPOLICE_ESTABLISH_STITCH_LINE_OF_FIRE;
+ }
+
+ return SCHED_NONE;
+}
+
+
+//-----------------------------------------------------------------------------
+// Combat schedule selection
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectAirboatRangeAttackSchedule()
+{
+ // Move to a ledge, if we need to.
+ int nSched = SelectMoveToLedgeSchedule();
+ if ( nSched != SCHED_NONE )
+ return nSched;
+
+ if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) )
+ {
+ nSched = SelectStitchSchedule();
+ if ( nSched != SCHED_NONE )
+ {
+ m_LastShootSlot = SQUAD_SLOT_NONE;
+ return nSched;
+ }
+ }
+
+ if( CanDeployManhack() && OccupyStrategySlot( SQUAD_SLOT_POLICE_DEPLOY_MANHACK ) )
+ {
+ return SCHED_METROPOLICE_DEPLOY_MANHACK;
+ }
+
+ return SCHED_METROPOLICE_ESTABLISH_LINE_OF_FIRE;
+}
+
+
+//-----------------------------------------------------------------------------
+// Combat schedule selection for when the enemy is in an airboat
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectAirboatCombatSchedule()
+{
+ int nResult = SelectScheduleNewEnemy();
+ if ( nResult != SCHED_NONE )
+ return nResult;
+
+ // We're assuming here that the cops who attack airboats have SMGs
+// Assert( Weapon_OwnsThisType( "weapon_smg1" ) );
+
+ if ( HasCondition( COND_SEE_ENEMY ) )
+ {
+ return SelectAirboatRangeAttackSchedule();
+ }
+
+ if ( HasCondition( COND_WEAPON_SIGHT_OCCLUDED ) )
+ {
+ // If they are hiding behind something also attack. Don't bother
+ // shooting the destroyable thing; it'll happen anyways with the SMG
+ CBaseEntity *pBlocker = GetEnemyOccluder();
+ if ( pBlocker && pBlocker->GetHealth() > 0 && OccupyStrategySlotRange( SQUAD_SLOT_POLICE_ATTACK_OCCLUDER1, SQUAD_SLOT_POLICE_ATTACK_OCCLUDER2 ) )
+ {
+ return SelectAirboatRangeAttackSchedule();
+ }
+ }
+
+ nResult = SelectScheduleNoDirectEnemy();
+ if ( nResult != SCHED_NONE )
+ return nResult;
+
+ return SCHED_NONE;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &info -
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::IsHeavyDamage( const CTakeDamageInfo &info )
+{
+ // Metropolice considers bullet fire heavy damage
+ if ( info.GetDamageType() & DMG_BULLET )
+ return true;
+
+ return BaseClass::IsHeavyDamage( info );
+}
+
+//-----------------------------------------------------------------------------
+// TraceAttack
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator )
+{
+ // This is needed so we can keep track of the direction of the shot
+ // because we're going to use it to choose the flinch animation
+ if ( m_bSimpleCops )
+ {
+ if ( m_takedamage == DAMAGE_YES )
+ {
+ Vector vecLastHitDirection;
+ VectorIRotate( vecDir, EntityToWorldTransform(), vecLastHitDirection );
+
+ // Point *at* the shooter
+ vecLastHitDirection *= -1.0f;
+
+ QAngle lastHitAngles;
+ VectorAngles( vecLastHitDirection, lastHitAngles );
+ m_flLastHitYaw = lastHitAngles.y;
+ }
+ }
+
+ BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator );
+}
+
+//-----------------------------------------------------------------------------
+// Determines the best type of flinch anim to play.
+//-----------------------------------------------------------------------------
+Activity CNPC_MetroPolice::GetFlinchActivity( bool bHeavyDamage, bool bGesture )
+{
+ if ( !bGesture && m_bSimpleCops )
+ {
+ // Version for getting shot from behind
+ if ( ( m_flLastHitYaw > 90 ) && ( m_flLastHitYaw < 270 ) )
+ {
+ Activity flinchActivity = (Activity)ACT_METROPOLICE_FLINCH_BEHIND;
+ if ( SelectWeightedSequence ( flinchActivity ) != ACTIVITY_NOT_AVAILABLE )
+ return flinchActivity;
+ }
+
+ if ( ( LastHitGroup() == HITGROUP_CHEST ) ||
+ ( LastHitGroup() == HITGROUP_LEFTLEG ) ||
+ ( LastHitGroup() == HITGROUP_RIGHTLEG ) )
+ {
+ Activity flinchActivity = ACT_FLINCH_STOMACH;
+ if ( SelectWeightedSequence ( ACT_FLINCH_STOMACH ) != ACTIVITY_NOT_AVAILABLE )
+ return flinchActivity;
+ }
+ }
+
+ return BaseClass::GetFlinchActivity( bHeavyDamage, bGesture );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::PlayFlinchGesture( void )
+{
+ BaseClass::PlayFlinchGesture();
+
+ // To ensure old playtested difficulty stays the same, stop cops shooting for a bit after gesture flinches
+ GetShotRegulator()->FireNoEarlierThan( gpGlobals->curtime + 0.5 );
+}
+
+//-----------------------------------------------------------------------------
+// We're taking cover from danger
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::AnnounceHarrassment( void )
+{
+ static const char *pWarnings[3] =
+ {
+ "METROPOLICE_BACK_UP_A",
+ "METROPOLICE_BACK_UP_B",
+ "METROPOLICE_BACK_UP_C",
+ };
+
+ m_Sentences.Speak( pWarnings[ random->RandomInt( 0, ARRAYSIZE(pWarnings)-1 ) ], SENTENCE_PRIORITY_MEDIUM, SENTENCE_CRITERIA_NORMAL );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::IncrementPlayerCriminalStatus( void )
+{
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 );
+
+ if ( pPlayer )
+ {
+ AddLookTarget( pPlayer, 0.8f, 5.0f );
+
+ if ( m_nNumWarnings < METROPOLICE_MAX_WARNINGS )
+ {
+ m_nNumWarnings++;
+ }
+
+ if ( m_nNumWarnings >= (METROPOLICE_MAX_WARNINGS-1) )
+ {
+ SetTarget( pPlayer );
+ SetBatonState( true );
+ }
+ }
+
+ m_flBatonDebounceTime = gpGlobals->curtime + random->RandomFloat( 2.0f, 4.0f );
+
+ AnnounceHarrassment();
+
+ m_bKeepFacingPlayer = true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : int
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectShoveSchedule( void )
+{
+ IncrementPlayerCriminalStatus();
+
+ // Stop chasing the player now that we've taken a swing at them
+ m_flChasePlayerTime = 0;
+ return SCHED_METROPOLICE_SHOVE;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : float
+//-----------------------------------------------------------------------------
+float CNPC_MetroPolice::GetIdealAccel( void ) const
+{
+ return GetIdealSpeed() * 2.0f;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Chase after a player who's just pissed us off, and hit him
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::AdministerJustice( void )
+{
+ if ( !AI_IsSinglePlayer() )
+ return;
+
+ // If we're allowed to chase the player, do so. Otherwise, just threaten.
+ if ( !IsInAScript() && (m_NPCState != NPC_STATE_SCRIPT) && HasSpawnFlags( SF_METROPOLICE_ALLOWED_TO_RESPOND ) )
+ {
+ if ( m_vecPreChaseOrigin == vec3_origin )
+ {
+ m_vecPreChaseOrigin = GetAbsOrigin();
+ m_flPreChaseYaw = GetAbsAngles().y;
+ }
+ m_flChasePlayerTime = gpGlobals->curtime + RandomFloat( 3, 7 );
+
+ // Attack the target
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex(1);
+ SetEnemy( pPlayer );
+ SetState( NPC_STATE_COMBAT );
+ UpdateEnemyMemory( pPlayer, pPlayer->GetAbsOrigin() );
+ }
+ else
+ {
+ // Watch the player for a time.
+ m_bKeepFacingPlayer = true;
+
+ // Try and find a nearby cop to administer justice
+ CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs();
+ int nAIs = g_AI_Manager.NumAIs();
+ for ( int i = 0; i < nAIs; i++ )
+ {
+ if ( ppAIs[i] == this )
+ continue;
+
+ if ( ppAIs[i]->Classify() == CLASS_METROPOLICE && FClassnameIs( ppAIs[i], "npc_metropolice" ) )
+ {
+ CNPC_MetroPolice *pNPC = assert_cast<CNPC_MetroPolice*>(ppAIs[i]);
+ if ( pNPC->HasSpawnFlags( SF_METROPOLICE_ALLOWED_TO_RESPOND ) )
+ {
+ // Is he within site & range?
+ if ( FVisible(pNPC) && pNPC->FVisible( UTIL_PlayerByIndex(1) ) &&
+ UTIL_DistApprox( WorldSpaceCenter(), pNPC->WorldSpaceCenter() ) < 512 )
+ {
+ pNPC->AdministerJustice();
+ break;
+ }
+ }
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Schedule selection
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectSchedule( void )
+{
+ if ( !GetEnemy() && HasCondition( COND_IN_PVS ) && AI_GetSinglePlayer() && !AI_GetSinglePlayer()->IsAlive() )
+ {
+ return SCHED_PATROL_WALK;
+ }
+
+ if ( HasCondition(COND_METROPOLICE_ON_FIRE) )
+ {
+ m_Sentences.Speak( "METROPOLICE_ON_FIRE", SENTENCE_PRIORITY_INVALID, SENTENCE_CRITERIA_ALWAYS );
+ return SCHED_METROPOLICE_BURNING_STAND;
+ }
+
+ // React to being struck by a physics object
+ if ( HasCondition( COND_METROPOLICE_PHYSOBJECT_ASSAULT ) )
+ {
+ ClearCondition( COND_METROPOLICE_PHYSOBJECT_ASSAULT );
+
+ // See which state our player relationship is in
+ if ( PlayerIsCriminal() == false )
+ {
+ m_Sentences.Speak( "METROPOLICE_HIT_BY_PHYSOBJECT", SENTENCE_PRIORITY_INVALID, SENTENCE_CRITERIA_ALWAYS );
+ m_nNumWarnings = METROPOLICE_MAX_WARNINGS;
+ AdministerJustice();
+ }
+ else if ( GlobalEntity_GetState( "gordon_precriminal" ) == GLOBAL_ON )
+ {
+ // We're not allowed to respond, but warn them
+ m_Sentences.Speak( "METROPOLICE_IDLE_HARASS_PLAYER", SENTENCE_PRIORITY_INVALID, SENTENCE_CRITERIA_ALWAYS );
+ }
+ }
+
+ int nSched = SelectFlinchSchedule();
+ if ( nSched != SCHED_NONE )
+ return nSched;
+
+ if ( HasBaton() )
+ {
+ // See if we're being told to activate our baton
+ if ( m_bShouldActivateBaton && BatonActive() == false && IsCurSchedule( SCHED_METROPOLICE_ACTIVATE_BATON ) == false )
+ return SCHED_METROPOLICE_ACTIVATE_BATON;
+
+ if ( m_bShouldActivateBaton == false && BatonActive() && IsCurSchedule( SCHED_METROPOLICE_DEACTIVATE_BATON ) == false )
+ return SCHED_METROPOLICE_DEACTIVATE_BATON;
+
+ if( metropolice_chase_use_follow.GetBool() )
+ {
+ if( GetEnemy() )
+ {
+ AI_FollowParams_t params;
+ params.formation = AIF_TIGHT;
+ m_FollowBehavior.SetParameters( params );
+ m_FollowBehavior.SetFollowTarget( GetEnemy() );
+ }
+ }
+ }
+
+ // See if the player is in our face (unless we're scripting)
+ if ( PlayerIsCriminal() == false )
+ {
+ if ( !IsInAScript() && (HasCondition( COND_METROPOLICE_PLAYER_TOO_CLOSE ) || m_bPlayerTooClose) )
+ {
+ // Don't hit the player too many times in a row, unless he's trying to push a cop who hasn't moved
+ if ( m_iNumPlayerHits < 3 || m_vecPreChaseOrigin == vec3_origin )
+ {
+ ClearCondition( COND_METROPOLICE_PLAYER_TOO_CLOSE );
+ m_bPlayerTooClose = false;
+
+ return SelectShoveSchedule();
+ }
+ }
+ else if ( m_iNumPlayerHits )
+ {
+ // If we're not in combat, and we've got a pre-chase origin, move back to it
+ if ( ( m_NPCState != NPC_STATE_COMBAT ) &&
+ ( m_vecPreChaseOrigin != vec3_origin ) &&
+ ( m_flChasePlayerTime < gpGlobals->curtime ) )
+ {
+ return SCHED_METROPOLICE_RETURN_TO_PRECHASE;
+ }
+ }
+ }
+
+ // Cower when physics objects are thrown at me
+ if ( HasCondition( COND_HEAR_PHYSICS_DANGER ) )
+ {
+ if ( m_flLastPhysicsFlinchTime + 4.0f <= gpGlobals->curtime )
+ {
+ m_flLastPhysicsFlinchTime = gpGlobals->curtime;
+ return SCHED_FLINCH_PHYSICS;
+ }
+ }
+
+ // Always run for cover from danger sounds
+ if ( HasCondition(COND_HEAR_DANGER) )
+ {
+ CSound *pSound;
+ pSound = GetBestSound();
+
+ Assert( pSound != NULL );
+ if ( pSound )
+ {
+ if (pSound->m_iType & SOUND_DANGER)
+ {
+ AnnounceTakeCoverFromDanger( pSound );
+ return SCHED_TAKE_COVER_FROM_BEST_SOUND;
+ }
+ if (!HasCondition( COND_SEE_ENEMY ) && ( pSound->m_iType & (SOUND_PLAYER | SOUND_PLAYER_VEHICLE | SOUND_COMBAT) ))
+ {
+ GetMotor()->SetIdealYawToTarget( pSound->GetSoundReactOrigin() );
+ }
+ }
+ }
+
+ bool bHighHealth = ((float)GetHealth() / (float)GetMaxHealth() > 0.75f);
+
+ // This will cause the cops to run backwards + shoot at the same time
+ if ( !bHighHealth && !HasBaton() )
+ {
+ if ( GetActiveWeapon() && (GetActiveWeapon()->m_iClip1 <= 5) )
+ {
+ m_Sentences.Speak( "METROPOLICE_COVER_LOW_AMMO" );
+ return SCHED_HIDE_AND_RELOAD;
+ }
+ }
+
+ if( HasCondition( COND_NO_PRIMARY_AMMO ) )
+ {
+ if ( bHighHealth )
+ return SCHED_RELOAD;
+
+ AnnounceOutOfAmmo( );
+ return SCHED_HIDE_AND_RELOAD;
+ }
+
+ // If we're clubbing someone who threw something at us. chase them
+ if ( m_NPCState == NPC_STATE_COMBAT && m_flChasePlayerTime > gpGlobals->curtime )
+ return SCHED_CHASE_ENEMY;
+
+ if ( !BehaviorSelectSchedule() )
+ {
+ // If we've warned the player at all, watch him like a hawk
+ if ( m_bKeepFacingPlayer && !PlayerIsCriminal() )
+ return SCHED_TARGET_FACE;
+
+ switch( m_NPCState )
+ {
+ case NPC_STATE_IDLE:
+ {
+ nSched = SelectScheduleInvestigateSound();
+ if ( nSched != SCHED_NONE )
+ return nSched;
+ break;
+ }
+
+ case NPC_STATE_ALERT:
+ {
+ nSched = SelectScheduleInvestigateSound();
+ if ( nSched != SCHED_NONE )
+ return nSched;
+ }
+ break;
+
+ case NPC_STATE_COMBAT:
+ if (!IsEnemyInAnAirboat() || !Weapon_OwnsThisType( "weapon_smg1" ) )
+ {
+ int nResult = SelectCombatSchedule();
+ if ( nResult != SCHED_NONE )
+ return nResult;
+ }
+ else
+ {
+ int nResult = SelectAirboatCombatSchedule();
+ if ( nResult != SCHED_NONE )
+ return nResult;
+ }
+ break;
+ }
+ }
+
+ // If we're not in combat, and we've got a pre-chase origin, move back to it
+ if ( ( m_NPCState != NPC_STATE_COMBAT ) &&
+ ( m_vecPreChaseOrigin != vec3_origin ) &&
+ ( m_flChasePlayerTime < gpGlobals->curtime ) )
+ {
+ return SCHED_METROPOLICE_RETURN_TO_PRECHASE;
+ }
+
+ return BaseClass::SelectSchedule();
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : failedSchedule -
+// failedTask -
+// taskFailCode -
+// Output : int
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode )
+{
+ if ( failedSchedule == SCHED_METROPOLICE_CHASE_ENEMY )
+ {
+ return SCHED_METROPOLICE_ESTABLISH_LINE_OF_FIRE;
+ }
+
+ return BaseClass::SelectFailSchedule( failedSchedule, failedTask, taskFailCode );
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::TranslateSchedule( int scheduleType )
+{
+ switch( scheduleType )
+ {
+ case SCHED_ALERT_FACE_BESTSOUND:
+ if ( !IsCurSchedule( SCHED_METROPOLICE_ALERT_FACE_BESTSOUND, false ) )
+ {
+ return SCHED_METROPOLICE_ALERT_FACE_BESTSOUND;
+ }
+ return SCHED_ALERT_FACE_BESTSOUND;
+
+ case SCHED_CHASE_ENEMY:
+
+ if ( !IsRunningBehavior() )
+ {
+ return SCHED_METROPOLICE_CHASE_ENEMY;
+ }
+
+ break;
+
+ case SCHED_ESTABLISH_LINE_OF_FIRE:
+ case SCHED_METROPOLICE_ESTABLISH_LINE_OF_FIRE:
+ if ( IsEnemyInAnAirboat() )
+ {
+ int nSched = SelectMoveToLedgeSchedule();
+ if ( nSched != SCHED_NONE )
+ return nSched;
+ }
+ return SCHED_METROPOLICE_ESTABLISH_LINE_OF_FIRE;
+
+ case SCHED_WAKE_ANGRY:
+ return SCHED_METROPOLICE_WAKE_ANGRY;
+
+ case SCHED_FAIL_TAKE_COVER:
+
+ if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) )
+ {
+ // Must be able to shoot now
+ if( TryToEnterPistolSlot( SQUAD_SLOT_ATTACK1 ) || TryToEnterPistolSlot( SQUAD_SLOT_ATTACK2 ) )
+ return SCHED_RANGE_ATTACK1;
+ }
+
+ if ( HasCondition( COND_NO_PRIMARY_AMMO ) )
+ return SCHED_RELOAD;
+ return SCHED_RUN_RANDOM;
+
+ case SCHED_RANGE_ATTACK1:
+ Assert( !HasCondition( COND_NO_PRIMARY_AMMO ) );
+
+ if( !m_fWeaponDrawn )
+ {
+ return SCHED_METROPOLICE_DRAW_PISTOL;
+ }
+
+ if( Weapon_OwnsThisType( "weapon_smg1" ) )
+ {
+ if ( IsEnemyInAnAirboat() )
+ {
+ int nSched = SelectStitchSchedule();
+ if ( nSched != SCHED_NONE )
+ return nSched;
+ }
+
+ if ( ShouldAttemptToStitch() )
+ {
+ return SCHED_METROPOLICE_SMG_BURST_ATTACK;
+ }
+ else
+ {
+ return SCHED_METROPOLICE_SMG_NORMAL_ATTACK;
+ }
+ }
+ break;
+ case SCHED_METROPOLICE_ADVANCE:
+ if ( m_NextChargeTimer.Expired() && metropolice_charge.GetBool() )
+ {
+ if ( Weapon_OwnsThisType( "weapon_pistol" ) )
+ {
+ if ( GetEnemy() && GetEnemy()->GetAbsOrigin().DistToSqr( GetAbsOrigin() ) > 300*300 )
+ {
+ if ( OccupyStrategySlot( SQUAD_SLOT_POLICE_CHARGE_ENEMY ) )
+ {
+ m_NextChargeTimer.Set( 3, 7 );
+ return SCHED_METROPOLICE_CHARGE;
+ }
+ }
+ }
+ else
+ {
+ m_NextChargeTimer.Set( 99999 );
+ }
+ }
+ break;
+ }
+
+
+ return BaseClass::TranslateSchedule( scheduleType );
+}
+
+
+//-----------------------------------------------------------------------------
+// Can't move and shoot when the enemy is an airboat
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::ShouldMoveAndShoot()
+{
+ if ( HasSpawnFlags( SF_METROPOLICE_ARREST_ENEMY ) )
+ return false;
+
+ if ( ShouldAttemptToStitch() )
+ return false;
+
+ return BaseClass::ShouldMoveAndShoot();
+}
+
+
+//-----------------------------------------------------------------------------
+// Only move and shoot when attacking
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::OnBeginMoveAndShoot()
+{
+ if ( BaseClass::OnBeginMoveAndShoot() )
+ {
+ if( HasStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ return true; // already have the slot I need
+
+ if( OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Only move and shoot when attacking
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::OnEndMoveAndShoot()
+{
+ VacateStrategySlot();
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : pTask -
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::StartTask( const Task_t *pTask )
+{
+ switch (pTask->iTask)
+ {
+ case TASK_METROPOLICE_WAIT_FOR_SENTENCE:
+ {
+ if ( FOkToMakeSound( pTask->flTaskData ) )
+ {
+ TaskComplete();
+ }
+ }
+ break;
+
+ case TASK_METROPOLICE_GET_PATH_TO_PRECHASE:
+ {
+ Assert( m_vecPreChaseOrigin != vec3_origin );
+ if ( GetNavigator()->SetGoal( m_vecPreChaseOrigin ) )
+ {
+ QAngle vecAngles( 0, m_flPreChaseYaw, 0 );
+ GetNavigator()->SetArrivalDirection( vecAngles );
+ TaskComplete();
+ }
+ else
+ {
+ TaskFail( FAIL_NO_ROUTE );
+ }
+ break;
+ }
+
+ case TASK_METROPOLICE_CLEAR_PRECHASE:
+ {
+ m_vecPreChaseOrigin = vec3_origin;
+ m_flPreChaseYaw = 0;
+ TaskComplete();
+ break;
+ }
+
+ case TASK_METROPOLICE_ACTIVATE_BATON:
+ {
+ // Simply early out if we're in here without a baton
+ if ( HasBaton() == false )
+ {
+ TaskComplete();
+ break;
+ }
+
+ bool activate = ( pTask->flTaskData != 0 );
+
+ if ( activate )
+ {
+ if ( BatonActive() || m_bShouldActivateBaton == false )
+ {
+ TaskComplete();
+ break;
+ }
+
+ m_Sentences.Speak( "METROPOLICE_ACTIVATE_BATON", SENTENCE_PRIORITY_NORMAL, SENTENCE_CRITERIA_NORMAL );
+ SetIdealActivity( (Activity) ACT_ACTIVATE_BATON );
+ }
+ else
+ {
+ if ( BatonActive() == false || m_bShouldActivateBaton )
+ {
+ TaskComplete();
+ break;
+ }
+
+ m_Sentences.Speak( "METROPOLICE_DEACTIVATE_BATON", SENTENCE_PRIORITY_NORMAL, SENTENCE_CRITERIA_NORMAL );
+ SetIdealActivity( (Activity) ACT_DEACTIVATE_BATON );
+ }
+ }
+ break;
+
+ case TASK_METROPOLICE_DIE_INSTANTLY:
+ {
+ CTakeDamageInfo info;
+
+ info.SetAttacker( this );
+ info.SetInflictor( this );
+ info.SetDamage( m_iHealth );
+ info.SetDamageType( pTask->flTaskData );
+ info.SetDamageForce( Vector( 0.1, 0.1, 0.1 ) );
+
+ TakeDamage( info );
+
+ TaskComplete();
+ }
+ break;
+
+ case TASK_METROPOLICE_RESET_LEDGE_CHECK_TIME:
+ m_flNextLedgeCheckTime = gpGlobals->curtime;
+ TaskComplete();
+ break;
+
+ case TASK_METROPOLICE_LEAD_ARREST_ENEMY:
+ case TASK_METROPOLICE_ARREST_ENEMY:
+ m_flTaskCompletionTime = gpGlobals->curtime + pTask->flTaskData;
+ break;
+
+ case TASK_METROPOLICE_SIGNAL_FIRING_TIME:
+ EnemyResistingArrest();
+ TaskComplete();
+ break;
+
+ case TASK_METROPOLICE_GET_PATH_TO_STITCH:
+ {
+ if ( !ShouldAttemptToStitch() )
+ {
+ TaskFail( FAIL_NO_ROUTE );
+ break;
+ }
+
+ Vector vecTarget, vecTargetVel;
+ PredictShootTargetPosition( 0.5f, 0.0f, 0.0f, &vecTarget, &vecTargetVel );
+
+ vecTarget -= GetAbsOrigin();
+ vecTarget.z = 0.0f;
+ float flDist = VectorNormalize( vecTarget );
+ if ( GetNavigator()->SetVectorGoal( vecTarget, flDist ) )
+ {
+ TaskComplete();
+ }
+ else
+ {
+ TaskFail( FAIL_NO_ROUTE );
+ }
+ }
+ break;
+
+ // Stitching aiming
+ case TASK_METROPOLICE_AIM_STITCH_TIGHTLY:
+ SetBurstMode( true );
+ AimBurstTightGrouping( pTask->flTaskData );
+ TaskComplete();
+ break;
+
+ case TASK_METROPOLICE_AIM_STITCH_AT_PLAYER:
+ SetBurstMode( true );
+ AimBurstAtEnemy( pTask->flTaskData );
+ TaskComplete();
+ break;
+
+ case TASK_METROPOLICE_AIM_STITCH_AT_AIRBOAT:
+ if ( IsEnemyInAnAirboat() )
+ {
+ SetBurstMode( true );
+ AimBurstAtEnemy( pTask->flTaskData );
+ TaskComplete();
+ }
+ else
+ {
+ TaskFail(FAIL_NO_TARGET);
+ }
+ break;
+
+ case TASK_METROPOLICE_AIM_STITCH_IN_FRONT_OF_AIRBOAT:
+ if ( IsEnemyInAnAirboat() )
+ {
+ SetBurstMode( true );
+ AimBurstInFrontOfEnemy( pTask->flTaskData );
+ TaskComplete();
+ }
+ else
+ {
+ TaskFail(FAIL_NO_TARGET);
+ }
+ break;
+
+ case TASK_METROPOLICE_AIM_STITCH_ALONG_SIDE_OF_AIRBOAT:
+ if ( IsEnemyInAnAirboat() )
+ {
+ SetBurstMode( true );
+ AimBurstAlongSideOfEnemy( pTask->flTaskData );
+ TaskComplete();
+ }
+ else
+ {
+ TaskFail(FAIL_NO_TARGET);
+ }
+ break;
+
+ case TASK_METROPOLICE_AIM_STITCH_BEHIND_AIRBOAT:
+ if ( IsEnemyInAnAirboat() )
+ {
+ SetBurstMode( true );
+ AimBurstBehindEnemy( pTask->flTaskData );
+ TaskComplete();
+ }
+ else
+ {
+ TaskFail(FAIL_NO_TARGET);
+ }
+ break;
+
+ case TASK_METROPOLICE_BURST_ATTACK:
+ ResetIdealActivity( ACT_RANGE_ATTACK1 );
+ break;
+
+ case TASK_METROPOLICE_STOP_FIRE_BURST:
+ {
+ SetBurstMode( false );
+ TaskComplete();
+ }
+ break;
+
+ case TASK_METROPOLICE_HARASS:
+ {
+ if( !( m_spawnflags & SF_METROPOLICE_NOCHATTER ) )
+ {
+ if( GetEnemy() && GetEnemy()->GetWaterLevel() > 0 )
+ {
+ EmitSound( "NPC_MetroPolice.WaterSpeech" );
+ }
+ else
+ {
+ EmitSound( "NPC_MetroPolice.HidingSpeech" );
+ }
+ }
+
+ TaskComplete();
+ }
+ break;
+
+ case TASK_METROPOLICE_RELOAD_FOR_BURST:
+ {
+ if (GetActiveWeapon())
+ {
+ int nDesiredShotCount = CountShotsInTime( pTask->flTaskData );
+
+ // Do our fake reload to simulate a bigger clip without having to change the SMG1
+ int nAddCount = nDesiredShotCount - GetActiveWeapon()->Clip1();
+ if ( nAddCount > 0 )
+ {
+ if ( m_nBurstReloadCount >= nAddCount )
+ {
+ GetActiveWeapon()->m_iClip1 += nAddCount;
+ m_nBurstReloadCount -= nAddCount;
+ }
+ }
+
+ if ( nDesiredShotCount <= GetActiveWeapon()->Clip1() )
+ {
+ TaskComplete();
+ break;
+ }
+ }
+
+ // Fake a TASK_RELOAD to make sure we've got a full clip...
+ Task_t reloadTask;
+ reloadTask.iTask = TASK_RELOAD;
+ reloadTask.flTaskData = 0.0f;
+ StartTask( &reloadTask );
+ }
+ break;
+
+ case TASK_RELOAD:
+ m_nBurstReloadCount = METROPOLICE_BURST_RELOAD_COUNT;
+ BaseClass::StartTask( pTask );
+ break;
+
+ case TASK_METROPOLICE_GET_PATH_TO_BESTSOUND_LOS:
+ {
+ }
+ break;
+
+ default:
+ BaseClass::StartTask( pTask );
+ break;
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+//
+// Run tasks!
+//
+//-----------------------------------------------------------------------------
+
+
+//-----------------------------------------------------------------------------
+// He's resisting arrest!
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::EnemyResistingArrest()
+{
+ // Prevent any other arrest from being made in this squad
+ // and tell them all that the player is resisting arrest!
+
+ if ( m_pSquad != NULL )
+ {
+ AISquadIter_t iter;
+ CAI_BaseNPC *pSquadmate = m_pSquad->GetFirstMember( &iter );
+ while ( pSquadmate )
+ {
+ pSquadmate->RemoveSpawnFlags( SF_METROPOLICE_ARREST_ENEMY );
+ pSquadmate->SetCondition( COND_METROPOLICE_ENEMY_RESISTING_ARREST );
+ pSquadmate = m_pSquad->GetNextMember( &iter );
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : pTask -
+//-----------------------------------------------------------------------------
+#define FLEEING_DISTANCE_SQR (100 * 100)
+
+void CNPC_MetroPolice::RunTask( const Task_t *pTask )
+{
+ switch( pTask->iTask )
+ {
+ case TASK_WAIT_FOR_MOVEMENT:
+ BaseClass::RunTask( pTask );
+ break;
+
+ case TASK_METROPOLICE_WAIT_FOR_SENTENCE:
+ {
+ if ( FOkToMakeSound( pTask->flTaskData ) )
+ {
+ TaskComplete();
+ }
+ }
+ break;
+
+ case TASK_METROPOLICE_ACTIVATE_BATON:
+ AutoMovement();
+
+ if ( IsActivityFinished() )
+ {
+ TaskComplete();
+ }
+ break;
+
+ case TASK_METROPOLICE_BURST_ATTACK:
+ {
+ AutoMovement( );
+
+ Vector vecAimPoint;
+ GetMotor()->SetIdealYawToTargetAndUpdate( m_vecBurstTargetPos, AI_KEEP_YAW_SPEED );
+
+ if ( IsActivityFinished() )
+ {
+ if ( GetShotRegulator()->IsInRestInterval() )
+ {
+ TaskComplete();
+ }
+ else
+ {
+ OnRangeAttack1();
+ ResetIdealActivity( ACT_RANGE_ATTACK1 );
+ }
+ }
+ }
+ break;
+
+ case TASK_METROPOLICE_RELOAD_FOR_BURST:
+ {
+ // Fake a TASK_RELOAD
+ Task_t reloadTask;
+ reloadTask.iTask = TASK_RELOAD;
+ reloadTask.flTaskData = 0.0f;
+ RunTask( &reloadTask );
+ }
+ break;
+
+ case TASK_METROPOLICE_LEAD_ARREST_ENEMY:
+ case TASK_METROPOLICE_ARREST_ENEMY:
+ {
+ if ( !GetEnemy() )
+ {
+ TaskComplete();
+ break;
+ }
+
+ if ( gpGlobals->curtime >= m_flTaskCompletionTime )
+ {
+ TaskComplete();
+ break;
+ }
+
+ // Complete the arrest after the last squad member has a bead on the enemy
+ // But only if you're the first guy who saw him
+ if ( pTask->iTask == TASK_METROPOLICE_LEAD_ARREST_ENEMY )
+ {
+ int nArrestCount = SquadArrestCount();
+ if ( nArrestCount == m_pSquad->NumMembers() )
+ {
+ TaskComplete();
+ break;
+ }
+
+ // Do a distance check of the enemy from his initial position.
+ // Shoot if he gets too far.
+ if ( m_vSavePosition.DistToSqr( GetEnemy()->GetAbsOrigin() ) > FLEEING_DISTANCE_SQR )
+ {
+ SpeakSentence( METROPOLICE_SENTENCE_HES_RUNNING );
+ EnemyResistingArrest();
+ break;
+ }
+ }
+
+ // Keep aiming at the enemy
+ if ( GetEnemy() && FacingIdeal() )
+ {
+ float flNewIdealYaw = CalcIdealYaw( GetEnemy()->EyePosition() );
+ if ( fabs(UTIL_AngleDiff( GetMotor()->GetIdealYaw(), flNewIdealYaw )) >= 45.0f )
+ {
+ GetMotor()->SetIdealYawToTarget( GetEnemy()->EyePosition() );
+ SetTurnActivity();
+ }
+ }
+ GetMotor()->UpdateYaw();
+ }
+ break;
+
+ case TASK_METROPOLICE_GET_PATH_TO_BESTSOUND_LOS:
+ {
+ switch( GetTaskInterrupt() )
+ {
+ case 0:
+ {
+ CSound *pSound = GetBestSound();
+ if (!pSound)
+ {
+ TaskFail(FAIL_NO_SOUND);
+ }
+ else
+ {
+ float flMaxRange = 2000;
+ float flMinRange = 0;
+ if ( GetActiveWeapon() )
+ {
+ flMaxRange = MAX( GetActiveWeapon()->m_fMaxRange1, GetActiveWeapon()->m_fMaxRange2 );
+ flMinRange = MIN( GetActiveWeapon()->m_fMinRange1, GetActiveWeapon()->m_fMinRange2 );
+ }
+
+ // Check against NPC's max range
+ if (flMaxRange > m_flDistTooFar)
+ {
+ flMaxRange = m_flDistTooFar;
+ }
+
+ // Why not doing lateral LOS first?
+
+ Vector losTarget = pSound->GetSoundReactOrigin();
+ if ( GetTacticalServices()->FindLos( pSound->GetSoundReactOrigin(), losTarget, flMinRange, flMaxRange, 1.0, &m_vInterruptSavePosition ) )
+ {
+ TaskInterrupt();
+ }
+ else
+ {
+ TaskFail(FAIL_NO_SHOOT);
+ }
+ }
+ }
+ break;
+
+ case 1:
+ {
+ AI_NavGoal_t goal( m_vInterruptSavePosition, ACT_RUN, AIN_HULL_TOLERANCE );
+ GetNavigator()->SetGoal( goal );
+ }
+ break;
+ }
+ }
+ break;
+
+ default:
+ BaseClass::RunTask( pTask );
+ break;
+ }
+}
+
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : pevInflictor -
+// pAttacker -
+// flDamage -
+// bitsDamageType -
+// Output : int
+//-----------------------------------------------------------------------------
+int CNPC_MetroPolice::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo )
+{
+ CTakeDamageInfo info = inputInfo;
+
+ if ( HasSpawnFlags( SF_METROPOLICE_ARREST_ENEMY ) )
+ {
+ EnemyResistingArrest();
+ }
+
+#if 0
+ // Die instantly from a hit in idle/alert states
+ if( m_NPCState == NPC_STATE_IDLE || m_NPCState == NPC_STATE_ALERT )
+ {
+ info.SetDamage( m_iHealth );
+ }
+#endif //0
+
+ if (info.GetAttacker() == GetEnemy())
+ {
+ // Keep track of recent damage by my attacker. If it seems like we're
+ // being killed, consider running off and hiding.
+ m_nRecentDamage += info.GetDamage();
+ m_flRecentDamageTime = gpGlobals->curtime;
+ }
+
+ return BaseClass::OnTakeDamage_Alive( info );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: I want to deploy a manhack. Can I?
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::CanDeployManhack( void )
+{
+ if ( HasSpawnFlags( SF_METROPOLICE_NO_MANHACK_DEPLOY ) )
+ return false;
+
+ // Nope, already have one out.
+ if( m_hManhack != NULL )
+ return false;
+
+ // Nope, don't have any!
+ if( m_iManhacks < 1 )
+ return false;
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Allows for modification of the interrupt mask for the current schedule.
+// In the most cases the base implementation should be called first.
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::BuildScheduleTestBits( void )
+{
+ BaseClass::BuildScheduleTestBits();
+
+ if ( PlayerIsCriminal() == false )
+ {
+ SetCustomInterruptCondition( COND_METROPOLICE_PHYSOBJECT_ASSAULT );
+ }
+
+ //FIXME: Always interrupt for now
+ if ( !IsInAScript() &&
+ !IsCurSchedule( SCHED_METROPOLICE_SHOVE ) &&
+ !IsCurSchedule( SCHED_MELEE_ATTACK1 ) &&
+ !IsCurSchedule( SCHED_RELOAD ) &&
+ !IsCurSchedule( SCHED_METROPOLICE_ACTIVATE_BATON ) )
+ {
+ SetCustomInterruptCondition( COND_METROPOLICE_PLAYER_TOO_CLOSE );
+ }
+
+ if ( !IsCurSchedule( SCHED_METROPOLICE_BURNING_RUN ) && !IsCurSchedule( SCHED_METROPOLICE_BURNING_STAND ) && !IsMoving() )
+ {
+ SetCustomInterruptCondition( COND_METROPOLICE_ON_FIRE );
+ }
+
+ if (IsCurSchedule(SCHED_TAKE_COVER_FROM_ENEMY))
+ {
+ ClearCustomInterruptCondition( COND_LIGHT_DAMAGE );
+ ClearCustomInterruptCondition( COND_HEAVY_DAMAGE );
+ }
+
+ if ( !IsCurSchedule( SCHED_CHASE_ENEMY ) &&
+ !IsCurSchedule( SCHED_METROPOLICE_ACTIVATE_BATON ) &&
+ !IsCurSchedule( SCHED_METROPOLICE_DEACTIVATE_BATON ) &&
+ !IsCurSchedule( SCHED_METROPOLICE_SHOVE ) &&
+ !IsCurSchedule( SCHED_METROPOLICE_RETURN_TO_PRECHASE ) )
+ {
+ SetCustomInterruptCondition( COND_METROPOLICE_CHANGE_BATON_STATE );
+ }
+
+ if ( IsCurSchedule( SCHED_MELEE_ATTACK1 ) )
+ {
+ if ( gpGlobals->curtime - m_flLastDamageFlinchTime < 10.0 )
+ {
+ ClearCustomInterruptCondition( COND_LIGHT_DAMAGE );
+ ClearCustomInterruptCondition( COND_HEAVY_DAMAGE );
+ }
+ }
+ else if ( HasBaton() && IsCurSchedule( SCHED_COMBAT_FACE ) && !m_BatonSwingTimer.Expired() )
+ {
+ ClearCustomInterruptCondition( COND_CAN_MELEE_ATTACK1 );
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+WeaponProficiency_t CNPC_MetroPolice::CalcWeaponProficiency( CBaseCombatWeapon *pWeapon )
+{
+ if( FClassnameIs( pWeapon, "weapon_pistol" ) )
+ {
+ return WEAPON_PROFICIENCY_POOR;
+ }
+
+ if( FClassnameIs( pWeapon, "weapon_smg1" ) )
+ {
+ return WEAPON_PROFICIENCY_VERY_GOOD;
+ }
+
+ return BaseClass::CalcWeaponProficiency( pWeapon );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::GatherConditions( void )
+{
+ BaseClass::GatherConditions();
+
+ if ( m_bPlayerTooClose == false )
+ {
+ ClearCondition( COND_METROPOLICE_PLAYER_TOO_CLOSE );
+ }
+
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 );
+
+ // FIXME: Player can be NULL here during level transitions.
+ if ( !pPlayer )
+ return;
+
+ float distToPlayerSqr = ( pPlayer->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr();
+
+ // See if we're too close
+ if ( pPlayer->GetGroundEntity() == this )
+ {
+ // Always beat a player on our head
+ m_iNumPlayerHits = 0;
+ SetCondition( COND_METROPOLICE_PLAYER_TOO_CLOSE );
+ }
+ else if ( (distToPlayerSqr < (42.0f*42.0f) && FVisible(pPlayer)) )
+ {
+ // Ignore the player if we've been beating him, but not if we haven't moved
+ if ( m_iNumPlayerHits < 3 || m_vecPreChaseOrigin == vec3_origin )
+ {
+ SetCondition( COND_METROPOLICE_PLAYER_TOO_CLOSE );
+ }
+ }
+ else
+ {
+ ClearCondition( COND_METROPOLICE_PLAYER_TOO_CLOSE );
+
+ // Don't clear out the player hit count for a few seconds after we last hit him
+ // This avoids states where two metropolice have the player pinned between them.
+ if ( (gpGlobals->curtime - GetLastAttackTime()) > 3 )
+ {
+ m_iNumPlayerHits = 0;
+ }
+
+ m_bPlayerTooClose = false;
+ }
+
+ if( metropolice_move_and_melee.GetBool() )
+ {
+ if( IsMoving() && HasCondition(COND_CAN_MELEE_ATTACK1) && HasBaton() )
+ {
+ if ( m_BatonSwingTimer.Expired() )
+ {
+ m_BatonSwingTimer.Set( 1.0, 1.75 );
+
+ Activity activity = TranslateActivity( ACT_MELEE_ATTACK_SWING_GESTURE );
+ Assert( activity != ACT_INVALID );
+ AddGesture( activity );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::HasBaton( void )
+{
+ CBaseCombatWeapon *pWeapon = GetActiveWeapon();
+
+ if ( pWeapon )
+ return FClassnameIs( pWeapon, "weapon_stunstick" );
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::BatonActive( void )
+{
+#ifndef HL2MP
+
+ CWeaponStunStick *pStick = dynamic_cast<CWeaponStunStick *>(GetActiveWeapon());
+
+ if ( pStick )
+ return pStick->GetStunState();
+#endif
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : state -
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::SetBatonState( bool state )
+{
+ if ( !HasBaton() )
+ return;
+
+ if ( m_bShouldActivateBaton != state )
+ {
+ m_bShouldActivateBaton = state;
+ SetCondition( COND_METROPOLICE_CHANGE_BATON_STATE );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pSound -
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_MetroPolice::QueryHearSound( CSound *pSound )
+{
+ // Only behave differently if the player is pre-criminal
+ if ( PlayerIsCriminal() == false )
+ {
+ // If the person making the sound was a friend, don't respond
+ if ( pSound->IsSoundType( SOUND_DANGER ) && pSound->m_hOwner && IRelationType( pSound->m_hOwner ) == D_NU )
+ return false;
+ }
+
+ return BaseClass::QueryHearSound( pSound );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : index -
+// *pEvent -
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent )
+{
+ BaseClass::VPhysicsCollision( index, pEvent );
+
+ int otherIndex = !index;
+
+ CBaseEntity *pHitEntity = pEvent->pEntities[otherIndex];
+
+ if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_PLAYER_HELD )
+ {
+ CHL2_Player *pPlayer = dynamic_cast<CHL2_Player *>(UTIL_PlayerByIndex( 1 ));
+
+ // See if it's being held by the player
+ if ( pPlayer != NULL && pPlayer->IsHoldingEntity( pHitEntity ) )
+ {
+ //TODO: Play an angry sentence, "Get that outta here!"
+
+ if ( IsCurSchedule( SCHED_METROPOLICE_SHOVE ) == false )
+ {
+ SetCondition( COND_METROPOLICE_PLAYER_TOO_CLOSE );
+ m_bPlayerTooClose = true;
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pTarget -
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::StunnedTarget( CBaseEntity *pTarget )
+{
+ SetLastAttackTime( gpGlobals->curtime );
+
+ if ( pTarget && pTarget->IsPlayer() )
+ {
+ m_OnStunnedPlayer.FireOutput( this, this );
+ m_iNumPlayerHits++;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Use response for when the player is pre-criminal
+//-----------------------------------------------------------------------------
+void CNPC_MetroPolice::PrecriminalUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value )
+{
+ if ( IsInAScript() )
+ return;
+ // Don't respond if I'm busy hating the player
+ if ( IRelationType( pActivator ) == D_HT || ((GetState() != NPC_STATE_ALERT) && (GetState() != NPC_STATE_IDLE)) )
+ return;
+ if ( PlayerIsCriminal() )
+ return;
+
+ // Treat it like the player's bothered the cop
+ IncrementPlayerCriminalStatus();
+
+ // If we've hit max warnings, and we're allowed to chase, go for it
+ if ( m_nNumWarnings == METROPOLICE_MAX_WARNINGS )
+ {
+ AdministerJustice();
+ }
+}
+
+//-----------------------------------------------------------------------------
+//
+// Schedules
+//
+//-----------------------------------------------------------------------------
+AI_BEGIN_CUSTOM_NPC( npc_metropolice, CNPC_MetroPolice )
+
+ gm_flTimeLastSpokePeek = 0;
+
+ DECLARE_ANIMEVENT( AE_METROPOLICE_BATON_ON );
+ DECLARE_ANIMEVENT( AE_METROPOLICE_BATON_OFF );
+ DECLARE_ANIMEVENT( AE_METROPOLICE_SHOVE );
+ DECLARE_ANIMEVENT( AE_METROPOLICE_START_DEPLOY );
+ DECLARE_ANIMEVENT( AE_METROPOLICE_DRAW_PISTOL );
+ DECLARE_ANIMEVENT( AE_METROPOLICE_DEPLOY_MANHACK );
+
+ DECLARE_SQUADSLOT( SQUAD_SLOT_POLICE_CHARGE_ENEMY );
+ DECLARE_SQUADSLOT( SQUAD_SLOT_POLICE_HARASS );
+ DECLARE_SQUADSLOT( SQUAD_SLOT_POLICE_DEPLOY_MANHACK );
+ DECLARE_SQUADSLOT( SQUAD_SLOT_POLICE_ATTACK_OCCLUDER1 );
+ DECLARE_SQUADSLOT( SQUAD_SLOT_POLICE_ATTACK_OCCLUDER2 );
+ DECLARE_SQUADSLOT( SQUAD_SLOT_POLICE_ARREST_ENEMY );
+
+ DECLARE_ACTIVITY( ACT_METROPOLICE_DRAW_PISTOL );
+ DECLARE_ACTIVITY( ACT_METROPOLICE_DEPLOY_MANHACK );
+ DECLARE_ACTIVITY( ACT_METROPOLICE_FLINCH_BEHIND );
+ DECLARE_ACTIVITY( ACT_PUSH_PLAYER );
+ DECLARE_ACTIVITY( ACT_MELEE_ATTACK_THRUST );
+ DECLARE_ACTIVITY( ACT_ACTIVATE_BATON );
+ DECLARE_ACTIVITY( ACT_DEACTIVATE_BATON );
+ DECLARE_ACTIVITY( ACT_WALK_BATON );
+ DECLARE_ACTIVITY( ACT_IDLE_ANGRY_BATON );
+
+ DECLARE_INTERACTION( g_interactionMetrocopStartedStitch );
+ DECLARE_INTERACTION( g_interactionMetrocopIdleChatter );
+ DECLARE_INTERACTION( g_interactionMetrocopClearSentenceQueues );
+
+ DECLARE_TASK( TASK_METROPOLICE_HARASS );
+ DECLARE_TASK( TASK_METROPOLICE_DIE_INSTANTLY );
+ DECLARE_TASK( TASK_METROPOLICE_BURST_ATTACK );
+ DECLARE_TASK( TASK_METROPOLICE_STOP_FIRE_BURST );
+ DECLARE_TASK( TASK_METROPOLICE_AIM_STITCH_AT_PLAYER );
+ DECLARE_TASK( TASK_METROPOLICE_AIM_STITCH_AT_AIRBOAT );
+ DECLARE_TASK( TASK_METROPOLICE_AIM_STITCH_IN_FRONT_OF_AIRBOAT );
+ DECLARE_TASK( TASK_METROPOLICE_AIM_STITCH_TIGHTLY );
+ DECLARE_TASK( TASK_METROPOLICE_AIM_STITCH_ALONG_SIDE_OF_AIRBOAT );
+ DECLARE_TASK( TASK_METROPOLICE_AIM_STITCH_BEHIND_AIRBOAT );
+ DECLARE_TASK( TASK_METROPOLICE_RELOAD_FOR_BURST );
+ DECLARE_TASK( TASK_METROPOLICE_GET_PATH_TO_STITCH );
+ DECLARE_TASK( TASK_METROPOLICE_RESET_LEDGE_CHECK_TIME );
+ DECLARE_TASK( TASK_METROPOLICE_GET_PATH_TO_BESTSOUND_LOS );
+ DECLARE_TASK( TASK_METROPOLICE_ARREST_ENEMY );
+ DECLARE_TASK( TASK_METROPOLICE_LEAD_ARREST_ENEMY );
+ DECLARE_TASK( TASK_METROPOLICE_SIGNAL_FIRING_TIME );
+ DECLARE_TASK( TASK_METROPOLICE_ACTIVATE_BATON );
+ DECLARE_TASK( TASK_METROPOLICE_WAIT_FOR_SENTENCE );
+ DECLARE_TASK( TASK_METROPOLICE_GET_PATH_TO_PRECHASE );
+ DECLARE_TASK( TASK_METROPOLICE_CLEAR_PRECHASE );
+
+ DECLARE_CONDITION( COND_METROPOLICE_ON_FIRE );
+ DECLARE_CONDITION( COND_METROPOLICE_ENEMY_RESISTING_ARREST );
+// DECLARE_CONDITION( COND_METROPOLICE_START_POLICING );
+ DECLARE_CONDITION( COND_METROPOLICE_PLAYER_TOO_CLOSE );
+ DECLARE_CONDITION( COND_METROPOLICE_CHANGE_BATON_STATE );
+ DECLARE_CONDITION( COND_METROPOLICE_PHYSOBJECT_ASSAULT );
+
+
+ //=========================================================
+//=========================================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_WAKE_ANGRY,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
+ " TASK_FACE_ENEMY 0"
+ " "
+ " Interrupts"
+);
+
+
+//=========================================================
+// > InvestigateSound
+//
+// sends a monster to the location of the
+// sound that was just heard to check things out.
+//=========================================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_INVESTIGATE_SOUND,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_STORE_LASTPOSITION 0"
+ " TASK_METROPOLICE_GET_PATH_TO_BESTSOUND_LOS 0"
+ " TASK_FACE_IDEAL 0"
+// " TASK_SET_TOLERANCE_DISTANCE 32"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_STOP_MOVING 0"
+ " TASK_WAIT 5"
+ " TASK_GET_PATH_TO_LASTPOSITION 0"
+ " TASK_WALK_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_STOP_MOVING 0"
+ " TASK_CLEAR_LASTPOSITION 0"
+ " TASK_FACE_REASONABLE 0"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_SEE_FEAR"
+ " COND_SEE_ENEMY"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_HEAR_DANGER"
+);
+
+
+//=========================================================
+//=========================================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_HARASS,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_WAIT_FACE_ENEMY 6"
+ " TASK_METROPOLICE_HARASS 0"
+ " TASK_WAIT_PVS 0"
+ " "
+ " Interrupts"
+ " "
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_NEW_ENEMY"
+);
+
+
+//=========================================================
+//=========================================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_DRAW_PISTOL,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_METROPOLICE_DRAW_PISTOL"
+ " TASK_WAIT_FACE_ENEMY 0.1"
+ " "
+ " Interrupts"
+ " "
+);
+
+
+//=========================================================
+// > ChaseEnemy
+//=========================================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_CHASE_ENEMY,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_METROPOLICE_ESTABLISH_LINE_OF_FIRE"
+ " TASK_SET_TOLERANCE_DISTANCE 24"
+ " TASK_GET_CHASE_PATH_TO_ENEMY 300"
+ " TASK_SPEAK_SENTENCE 6" // METROPOLICE_SENTENCE_MOVE_INTO_POSITION
+ " TASK_RUN_PATH 0"
+ " TASK_METROPOLICE_RESET_LEDGE_CHECK_TIME 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_FACE_ENEMY 0"
+ " "
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_ENEMY_UNREACHABLE"
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_RANGE_ATTACK2"
+ " COND_CAN_MELEE_ATTACK2"
+ " COND_TOO_CLOSE_TO_ATTACK"
+ " COND_TASK_FAILED"
+ " COND_LOST_ENEMY"
+ " COND_BETTER_WEAPON_AVAILABLE"
+ " COND_HEAR_DANGER"
+);
+
+
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_ESTABLISH_LINE_OF_FIRE,
+
+ " Tasks "
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_FAIL_ESTABLISH_LINE_OF_FIRE"
+ " TASK_FACE_ENEMY 0"
+ " TASK_SET_TOLERANCE_DISTANCE 48"
+ " TASK_GET_PATH_TO_ENEMY_LKP_LOS 0"
+ " TASK_SPEAK_SENTENCE 6" // METROPOLICE_SENTENCE_MOVE_INTO_POSITION
+ " TASK_RUN_PATH 0"
+ " TASK_METROPOLICE_RESET_LEDGE_CHECK_TIME 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_COMBAT_FACE"
+ " "
+ " Interrupts "
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_CAN_RANGE_ATTACK2"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK2"
+ " COND_HEAR_DANGER"
+ " COND_HEAVY_DAMAGE"
+);
+
+
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_ESTABLISH_STITCH_LINE_OF_FIRE,
+
+ " Tasks "
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_FAIL_ESTABLISH_LINE_OF_FIRE"
+ " TASK_FACE_ENEMY 0"
+ " TASK_SET_TOLERANCE_DISTANCE 48"
+ " TASK_METROPOLICE_GET_PATH_TO_STITCH 0"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_COMBAT_FACE"
+ " "
+ " Interrupts "
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_HEAR_DANGER"
+ " COND_HEAVY_DAMAGE"
+);
+
+
+//=========================================================
+// The uninterruptible portion of this behavior, whereupon
+// the police actually releases the manhack.
+//=========================================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_DEPLOY_MANHACK,
+
+ " Tasks"
+ " TASK_SPEAK_SENTENCE 5" // METROPOLICE_SENTENCE_DEPLOY_MANHACK
+ " TASK_PLAY_SEQUENCE ACTIVITY:ACT_METROPOLICE_DEPLOY_MANHACK"
+ " "
+ " Interrupts"
+ " "
+);
+
+
+//===============================================
+//===============================================
+
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_ADVANCE,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE_ANGRY"
+ " TASK_FACE_ENEMY 0"
+ " TASK_WAIT_FACE_ENEMY 1" // give the guy some time to come out on his own
+ " TASK_WAIT_FACE_ENEMY_RANDOM 3"
+ " TASK_GET_PATH_TO_ENEMY_LOS 0"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE_ANGRY"
+ " TASK_FACE_ENEMY 0"
+ ""
+ " Interrupts"
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_ENEMY_DEAD"
+ ""
+);
+
+//===============================================
+//===============================================
+
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_CHARGE,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_METROPOLICE_ADVANCE"
+// " TASK_SET_TOLERANCE_DISTANCE 24"
+ " TASK_STORE_LASTPOSITION 0"
+ " TASK_GET_CHASE_PATH_TO_ENEMY 300"
+ " TASK_RUN_PATH_FOR_UNITS 150"
+ " TASK_STOP_MOVING 1"
+ " TASK_FACE_ENEMY 0"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_LOST_ENEMY"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK2"
+ " COND_HEAR_DANGER"
+ " COND_METROPOLICE_PLAYER_TOO_CLOSE"
+);
+
+//=========================================================
+//=========================================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_BURNING_RUN,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_METROPOLICE_BURNING_STAND"
+ " TASK_SET_TOLERANCE_DISTANCE 24"
+ " TASK_GET_PATH_TO_ENEMY 0"
+ " TASK_RUN_PATH_TIMED 10"
+ " TASK_METROPOLICE_DIE_INSTANTLY 0"
+ " "
+ " Interrupts"
+);
+
+//=========================================================
+//=========================================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_BURNING_STAND,
+
+ " Tasks"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE_ON_FIRE"
+ " TASK_WAIT 1.5"
+ " TASK_METROPOLICE_DIE_INSTANTLY DMG_BURN"
+ " TASK_WAIT 1.0"
+ " "
+ " Interrupts"
+);
+
+//=========================================================
+//=========================================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_RETURN_TO_PRECHASE,
+
+ " Tasks"
+ " TASK_WAIT_RANDOM 1"
+ " TASK_METROPOLICE_GET_PATH_TO_PRECHASE 0"
+ " TASK_WALK_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_STOP_MOVING 0"
+ " TASK_METROPOLICE_CLEAR_PRECHASE 0"
+ " "
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK2"
+ " COND_TASK_FAILED"
+ " COND_LOST_ENEMY"
+ " COND_HEAR_DANGER"
+);
+
+//===============================================
+//===============================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_ALERT_FACE_BESTSOUND,
+
+ " Tasks"
+ " TASK_SPEAK_SENTENCE 7" // METROPOLICE_SENTENCE_HEARD_SOMETHING
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_ALERT_FACE_BESTSOUND"
+ ""
+ " Interrupts"
+ ""
+)
+
+
+//===============================================
+//===============================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_ENEMY_RESISTING_ARREST,
+
+ " Tasks"
+ " TASK_METROPOLICE_SIGNAL_FIRING_TIME 0"
+ ""
+ " Interrupts"
+ ""
+)
+
+
+//===============================================
+//===============================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_WARN_AND_ARREST_ENEMY,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_METROPOLICE_ENEMY_RESISTING_ARREST"
+ " TASK_STOP_MOVING 0"
+ " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_IDLE_ANGRY"
+ " TASK_SPEAK_SENTENCE 0" // "Freeze!"
+ " TASK_METROPOLICE_ARREST_ENEMY 0.5"
+ " TASK_STORE_ENEMY_POSITION_IN_SAVEPOSITION 0"
+ " TASK_METROPOLICE_ARREST_ENEMY 1"
+ " TASK_METROPOLICE_WAIT_FOR_SENTENCE 1"
+ " TASK_SPEAK_SENTENCE 1" // "He's over here!"
+ " TASK_METROPOLICE_LEAD_ARREST_ENEMY 5"
+ " TASK_METROPOLICE_ARREST_ENEMY 2"
+ " TASK_METROPOLICE_WAIT_FOR_SENTENCE 1"
+ " TASK_SPEAK_SENTENCE 3" // "Take him down!"
+ " TASK_METROPOLICE_ARREST_ENEMY 1.5"
+ " TASK_METROPOLICE_WAIT_FOR_SENTENCE 2"
+ " TASK_METROPOLICE_SIGNAL_FIRING_TIME 0"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_HEAR_DANGER"
+ " COND_ENEMY_DEAD"
+ " COND_METROPOLICE_ENEMY_RESISTING_ARREST"
+ ""
+);
+
+//===============================================
+//===============================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_ARREST_ENEMY,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_METROPOLICE_ENEMY_RESISTING_ARREST"
+ " TASK_GET_PATH_TO_ENEMY_LOS 0"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_STOP_MOVING 0"
+ " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_IDLE_ANGRY"
+ " TASK_METROPOLICE_WAIT_FOR_SENTENCE 0"
+ " TASK_SPEAK_SENTENCE 4"
+ " TASK_METROPOLICE_ARREST_ENEMY 30"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_HEAR_DANGER"
+ " COND_ENEMY_DEAD"
+ " COND_METROPOLICE_ENEMY_RESISTING_ARREST"
+ " COND_WEAPON_BLOCKED_BY_FRIEND"
+ " COND_WEAPON_SIGHT_OCCLUDED"
+ ""
+);
+
+
+//===============================================
+//===============================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_SMG_NORMAL_ATTACK,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
+ " TASK_METROPOLICE_STOP_FIRE_BURST 0"
+ " TASK_RANGE_ATTACK1 0"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_ENEMY_OCCLUDED"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_HEAR_DANGER"
+ " COND_WEAPON_BLOCKED_BY_FRIEND"
+ " COND_WEAPON_SIGHT_OCCLUDED"
+);
+
+
+//===============================================
+//===============================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_SMG_BURST_ATTACK,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
+ " TASK_METROPOLICE_RELOAD_FOR_BURST 1.4"
+ " TASK_METROPOLICE_AIM_STITCH_AT_PLAYER 1.4"
+ " TASK_METROPOLICE_BURST_ATTACK 0"
+ " TASK_FACE_ENEMY 0"
+ ""
+ " Interrupts"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_HEAR_DANGER"
+ " COND_WEAPON_BLOCKED_BY_FRIEND"
+
+);
+
+//===============================================
+//===============================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_AIM_STITCH_TIGHTLY,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
+ " TASK_METROPOLICE_RELOAD_FOR_BURST 1.0"
+ " TASK_METROPOLICE_AIM_STITCH_TIGHTLY 1.0"
+ " TASK_METROPOLICE_BURST_ATTACK 0"
+ " TASK_FACE_ENEMY 0"
+ ""
+ " Interrupts"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_HEAR_DANGER"
+ " COND_WEAPON_BLOCKED_BY_FRIEND"
+
+);
+
+
+//===============================================
+//===============================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_AIM_STITCH_AT_AIRBOAT,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
+ " TASK_METROPOLICE_RELOAD_FOR_BURST 2.5"
+ " TASK_METROPOLICE_AIM_STITCH_AT_AIRBOAT 2.5"
+ " TASK_METROPOLICE_BURST_ATTACK 0"
+ " TASK_FACE_ENEMY 0"
+ ""
+ " Interrupts"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_HEAR_DANGER"
+ " COND_WEAPON_BLOCKED_BY_FRIEND"
+
+);
+
+//===============================================
+//===============================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_AIM_STITCH_IN_FRONT_OF_AIRBOAT,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
+ " TASK_METROPOLICE_RELOAD_FOR_BURST 2.5"
+ " TASK_METROPOLICE_AIM_STITCH_IN_FRONT_OF_AIRBOAT 2.5"
+ " TASK_METROPOLICE_BURST_ATTACK 0"
+ " TASK_FACE_ENEMY 0"
+ ""
+ " Interrupts"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_HEAR_DANGER"
+ " COND_WEAPON_BLOCKED_BY_FRIEND"
+
+);
+
+//===============================================
+//===============================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_AIM_STITCH_ALONG_SIDE_OF_AIRBOAT,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
+ " TASK_METROPOLICE_RELOAD_FOR_BURST 2.5"
+ " TASK_METROPOLICE_AIM_STITCH_ALONG_SIDE_OF_AIRBOAT 2.5"
+ " TASK_METROPOLICE_BURST_ATTACK 0"
+ " TASK_FACE_ENEMY 0"
+ ""
+ " Interrupts"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_HEAR_DANGER"
+ " COND_WEAPON_BLOCKED_BY_FRIEND"
+
+);
+
+//===============================================
+//===============================================
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_AIM_STITCH_BEHIND_AIRBOAT,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
+ " TASK_METROPOLICE_RELOAD_FOR_BURST 2.5"
+ " TASK_METROPOLICE_AIM_STITCH_BEHIND_AIRBOAT 2.5"
+ " TASK_METROPOLICE_BURST_ATTACK 0"
+ " TASK_FACE_ENEMY 0"
+ ""
+ " Interrupts"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_HEAR_DANGER"
+ " COND_WEAPON_BLOCKED_BY_FRIEND"
+
+);
+
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_SHOVE,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_PLAYER 0.1" //FIXME: This needs to be the target or enemy
+ " TASK_METROPOLICE_ACTIVATE_BATON 1"
+ " TASK_PLAY_SEQUENCE ACTIVITY:ACT_PUSH_PLAYER"
+ ""
+ " Interrupts"
+);
+
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_ACTIVATE_BATON,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_TARGET 0"
+ " TASK_METROPOLICE_ACTIVATE_BATON 1"
+ ""
+ " Interrupts"
+);
+
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_DEACTIVATE_BATON,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_METROPOLICE_ACTIVATE_BATON 0"
+ ""
+ " Interrupts"
+);
+
+DEFINE_SCHEDULE
+(
+ SCHED_METROPOLICE_SMASH_PROP,
+
+ " Tasks"
+ " TASK_GET_PATH_TO_TARGET 0"
+ " TASK_MOVE_TO_TARGET_RANGE 50"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_TARGET 0"
+ " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
+ " TASK_PLAY_SEQUENCE ACTIVITY:ACT_MELEE_ATTACK1"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+);
+
+AI_END_CUSTOM_NPC()
+
|