//========= 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( 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(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( 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( 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( 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 float 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(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(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(GetActiveWeapon()); if ( pStick ) { pStick->SetStunState( true ); } #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_MetroPolice::OnAnimEventBatonOff( void ) { #ifndef HL2MP CWeaponStunStick *pStick = dynamic_cast(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(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( 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(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(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(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(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()