summaryrefslogtreecommitdiff
path: root/game/server/hl2/npc_combine.cpp
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/hl2/npc_combine.cpp
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'game/server/hl2/npc_combine.cpp')
-rw-r--r--game/server/hl2/npc_combine.cpp4045
1 files changed, 4045 insertions, 0 deletions
diff --git a/game/server/hl2/npc_combine.cpp b/game/server/hl2/npc_combine.cpp
new file mode 100644
index 0000000..09cae88
--- /dev/null
+++ b/game/server/hl2/npc_combine.cpp
@@ -0,0 +1,4045 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+//=============================================================================//
+
+#include "cbase.h"
+#include "ai_hull.h"
+#include "ai_navigator.h"
+#include "ai_motor.h"
+#include "ai_squadslot.h"
+#include "ai_squad.h"
+#include "ai_route.h"
+#include "ai_interactions.h"
+#include "ai_tacticalservices.h"
+#include "soundent.h"
+#include "game.h"
+#include "npcevent.h"
+#include "npc_combine.h"
+#include "activitylist.h"
+#include "player.h"
+#include "basecombatweapon.h"
+#include "basegrenade_shared.h"
+#include "vstdlib/random.h"
+#include "engine/IEngineSound.h"
+#include "globals.h"
+#include "grenade_frag.h"
+#include "ndebugoverlay.h"
+#include "weapon_physcannon.h"
+#include "SoundEmitterSystem/isoundemittersystembase.h"
+#include "npc_headcrab.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+int g_fCombineQuestion; // true if an idle grunt asked a question. Cleared when someone answers. YUCK old global from grunt code
+
+#define COMBINE_SKIN_DEFAULT 0
+#define COMBINE_SKIN_SHOTGUNNER 1
+
+
+#define COMBINE_GRENADE_THROW_SPEED 650
+#define COMBINE_GRENADE_TIMER 3.5
+#define COMBINE_GRENADE_FLUSH_TIME 3.0 // Don't try to flush an enemy who has been out of sight for longer than this.
+#define COMBINE_GRENADE_FLUSH_DIST 256.0 // Don't try to flush an enemy who has moved farther than this distance from the last place I saw him.
+
+#define COMBINE_LIMP_HEALTH 20
+#define COMBINE_MIN_GRENADE_CLEAR_DIST 250
+
+#define COMBINE_EYE_STANDING_POSITION Vector( 0, 0, 66 )
+#define COMBINE_GUN_STANDING_POSITION Vector( 0, 0, 57 )
+#define COMBINE_EYE_CROUCHING_POSITION Vector( 0, 0, 40 )
+#define COMBINE_GUN_CROUCHING_POSITION Vector( 0, 0, 36 )
+#define COMBINE_SHOTGUN_STANDING_POSITION Vector( 0, 0, 36 )
+#define COMBINE_SHOTGUN_CROUCHING_POSITION Vector( 0, 0, 36 )
+#define COMBINE_MIN_CROUCH_DISTANCE 256.0
+
+//-----------------------------------------------------------------------------
+// Static stuff local to this file.
+//-----------------------------------------------------------------------------
+// This is the index to the name of the shotgun's classname in the string pool
+// so that we can get away with an integer compare rather than a string compare.
+string_t s_iszShotgunClassname;
+
+//-----------------------------------------------------------------------------
+// Interactions
+//-----------------------------------------------------------------------------
+int g_interactionCombineBash = 0; // melee bash attack
+
+//=========================================================
+// Combines's Anim Events Go Here
+//=========================================================
+#define COMBINE_AE_RELOAD ( 2 )
+#define COMBINE_AE_KICK ( 3 )
+#define COMBINE_AE_AIM ( 4 )
+#define COMBINE_AE_GREN_TOSS ( 7 )
+#define COMBINE_AE_GREN_LAUNCH ( 8 )
+#define COMBINE_AE_GREN_DROP ( 9 )
+#define COMBINE_AE_CAUGHT_ENEMY ( 10) // grunt established sight with an enemy (player only) that had previously eluded the squad.
+
+int COMBINE_AE_BEGIN_ALTFIRE;
+int COMBINE_AE_ALTFIRE;
+
+//=========================================================
+// Combine activities
+//=========================================================
+//Activity ACT_COMBINE_STANDING_SMG1;
+//Activity ACT_COMBINE_CROUCHING_SMG1;
+//Activity ACT_COMBINE_STANDING_AR2;
+//Activity ACT_COMBINE_CROUCHING_AR2;
+//Activity ACT_COMBINE_WALKING_AR2;
+//Activity ACT_COMBINE_STANDING_SHOTGUN;
+//Activity ACT_COMBINE_CROUCHING_SHOTGUN;
+Activity ACT_COMBINE_THROW_GRENADE;
+Activity ACT_COMBINE_LAUNCH_GRENADE;
+Activity ACT_COMBINE_BUGBAIT;
+Activity ACT_COMBINE_AR2_ALTFIRE;
+Activity ACT_WALK_EASY;
+Activity ACT_WALK_MARCH;
+
+// -----------------------------------------------
+// > Squad slots
+// -----------------------------------------------
+enum SquadSlot_T
+{
+ SQUAD_SLOT_GRENADE1 = LAST_SHARED_SQUADSLOT,
+ SQUAD_SLOT_GRENADE2,
+ SQUAD_SLOT_ATTACK_OCCLUDER,
+ SQUAD_SLOT_OVERWATCH,
+};
+
+enum TacticalVariant_T
+{
+ TACTICAL_VARIANT_DEFAULT = 0,
+ TACTICAL_VARIANT_PRESSURE_ENEMY, // Always try to close in on the player.
+ TACTICAL_VARIANT_PRESSURE_ENEMY_UNTIL_CLOSE, // Act like VARIANT_PRESSURE_ENEMY, but go to VARIANT_DEFAULT once within 30 feet
+};
+
+enum PathfindingVariant_T
+{
+ PATHFINDING_VARIANT_DEFAULT = 0,
+};
+
+
+#define bits_MEMORY_PAIN_LIGHT_SOUND bits_MEMORY_CUSTOM1
+#define bits_MEMORY_PAIN_HEAVY_SOUND bits_MEMORY_CUSTOM2
+#define bits_MEMORY_PLAYER_HURT bits_MEMORY_CUSTOM3
+
+LINK_ENTITY_TO_CLASS( npc_combine, CNPC_Combine );
+
+//---------------------------------------------------------
+// Save/Restore
+//---------------------------------------------------------
+BEGIN_DATADESC( CNPC_Combine )
+
+DEFINE_FIELD( m_nKickDamage, FIELD_INTEGER ),
+DEFINE_FIELD( m_vecTossVelocity, FIELD_VECTOR ),
+DEFINE_FIELD( m_hForcedGrenadeTarget, FIELD_EHANDLE ),
+DEFINE_FIELD( m_bShouldPatrol, FIELD_BOOLEAN ),
+DEFINE_FIELD( m_bFirstEncounter, FIELD_BOOLEAN ),
+DEFINE_FIELD( m_flNextPainSoundTime, FIELD_TIME ),
+DEFINE_FIELD( m_flNextAlertSoundTime, FIELD_TIME ),
+DEFINE_FIELD( m_flNextGrenadeCheck, FIELD_TIME ),
+DEFINE_FIELD( m_flNextLostSoundTime, FIELD_TIME ),
+DEFINE_FIELD( m_flAlertPatrolTime, FIELD_TIME ),
+DEFINE_FIELD( m_flNextAltFireTime, FIELD_TIME ),
+DEFINE_FIELD( m_nShots, FIELD_INTEGER ),
+DEFINE_FIELD( m_flShotDelay, FIELD_FLOAT ),
+DEFINE_FIELD( m_flStopMoveShootTime, FIELD_TIME ),
+DEFINE_KEYFIELD( m_iNumGrenades, FIELD_INTEGER, "NumGrenades" ),
+DEFINE_EMBEDDED( m_Sentences ),
+
+// m_AssaultBehavior (auto saved by AI)
+// m_StandoffBehavior (auto saved by AI)
+// m_FollowBehavior (auto saved by AI)
+// m_FuncTankBehavior (auto saved by AI)
+// m_RappelBehavior (auto saved by AI)
+// m_ActBusyBehavior (auto saved by AI)
+
+DEFINE_INPUTFUNC( FIELD_VOID, "LookOff", InputLookOff ),
+DEFINE_INPUTFUNC( FIELD_VOID, "LookOn", InputLookOn ),
+
+DEFINE_INPUTFUNC( FIELD_VOID, "StartPatrolling", InputStartPatrolling ),
+DEFINE_INPUTFUNC( FIELD_VOID, "StopPatrolling", InputStopPatrolling ),
+
+DEFINE_INPUTFUNC( FIELD_STRING, "Assault", InputAssault ),
+
+DEFINE_INPUTFUNC( FIELD_VOID, "HitByBugbait", InputHitByBugbait ),
+
+DEFINE_INPUTFUNC( FIELD_STRING, "ThrowGrenadeAtTarget", InputThrowGrenadeAtTarget ),
+
+DEFINE_FIELD( m_iLastAnimEventHandled, FIELD_INTEGER ),
+DEFINE_FIELD( m_fIsElite, FIELD_BOOLEAN ),
+DEFINE_FIELD( m_vecAltFireTarget, FIELD_VECTOR ),
+
+DEFINE_KEYFIELD( m_iTacticalVariant, FIELD_INTEGER, "tacticalvariant" ),
+DEFINE_KEYFIELD( m_iPathfindingVariant, FIELD_INTEGER, "pathfindingvariant" ),
+
+END_DATADESC()
+
+
+//------------------------------------------------------------------------------
+// Constructor.
+//------------------------------------------------------------------------------
+CNPC_Combine::CNPC_Combine()
+{
+ m_vecTossVelocity = vec3_origin;
+}
+
+
+//-----------------------------------------------------------------------------
+// Create components
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::CreateComponents()
+{
+ if ( !BaseClass::CreateComponents() )
+ return false;
+
+ m_Sentences.Init( this, "NPC_Combine.SentenceParameters" );
+ return true;
+}
+
+
+//------------------------------------------------------------------------------
+// Purpose: Don't look, only get info from squad.
+//------------------------------------------------------------------------------
+void CNPC_Combine::InputLookOff( inputdata_t &inputdata )
+{
+ m_spawnflags |= SF_COMBINE_NO_LOOK;
+}
+
+//------------------------------------------------------------------------------
+// Purpose: Enable looking.
+//------------------------------------------------------------------------------
+void CNPC_Combine::InputLookOn( inputdata_t &inputdata )
+{
+ m_spawnflags &= ~SF_COMBINE_NO_LOOK;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_Combine::InputStartPatrolling( inputdata_t &inputdata )
+{
+ m_bShouldPatrol = true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_Combine::InputStopPatrolling( inputdata_t &inputdata )
+{
+ m_bShouldPatrol = false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_Combine::InputAssault( inputdata_t &inputdata )
+{
+ m_AssaultBehavior.SetParameters( AllocPooledString(inputdata.value.String()), CUE_DONT_WAIT, RALLY_POINT_SELECT_DEFAULT );
+}
+
+//-----------------------------------------------------------------------------
+// We were hit by bugbait
+//-----------------------------------------------------------------------------
+void CNPC_Combine::InputHitByBugbait( inputdata_t &inputdata )
+{
+ SetCondition( COND_COMBINE_HIT_BY_BUGBAIT );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Force the combine soldier to throw a grenade at the target
+// If I'm a combine elite, fire my combine ball at the target instead.
+// Input : &inputdata -
+//-----------------------------------------------------------------------------
+void CNPC_Combine::InputThrowGrenadeAtTarget( inputdata_t &inputdata )
+{
+ // Ignore if we're inside a scripted sequence
+ if ( m_NPCState == NPC_STATE_SCRIPT && m_hCine )
+ return;
+
+ CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, inputdata.value.String(), NULL, inputdata.pActivator, inputdata.pCaller );
+ if ( !pEntity )
+ {
+ DevMsg("%s (%s) received ThrowGrenadeAtTarget input, but couldn't find target entity '%s'\n", GetClassname(), GetDebugName(), inputdata.value.String() );
+ return;
+ }
+
+ m_hForcedGrenadeTarget = pEntity;
+ m_flNextGrenadeCheck = 0;
+
+ ClearSchedule( "Told to throw grenade via input" );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_Combine::Precache()
+{
+ PrecacheModel("models/Weapons/w_grenade.mdl");
+ UTIL_PrecacheOther( "npc_handgrenade" );
+
+ PrecacheScriptSound( "NPC_Combine.GrenadeLaunch" );
+ PrecacheScriptSound( "NPC_Combine.WeaponBash" );
+ PrecacheScriptSound( "Weapon_CombineGuard.Special1" );
+
+ BaseClass::Precache();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_Combine::Activate()
+{
+ s_iszShotgunClassname = FindPooledString( "weapon_shotgun" );
+ BaseClass::Activate();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//
+//
+//-----------------------------------------------------------------------------
+void CNPC_Combine::Spawn( void )
+{
+ SetHullType(HULL_HUMAN);
+ SetHullSizeNormal();
+
+ SetSolid( SOLID_BBOX );
+ AddSolidFlags( FSOLID_NOT_STANDABLE );
+ SetMoveType( MOVETYPE_STEP );
+ SetBloodColor( BLOOD_COLOR_RED );
+ m_flFieldOfView = -0.2;// indicates the width of this NPC's forward view cone ( as a dotproduct result )
+ m_NPCState = NPC_STATE_NONE;
+ m_flNextGrenadeCheck = gpGlobals->curtime + 1;
+ m_flNextPainSoundTime = 0;
+ m_flNextAlertSoundTime = 0;
+ m_bShouldPatrol = false;
+
+ // CapabilitiesAdd( bits_CAP_TURN_HEAD | bits_CAP_MOVE_GROUND | bits_CAP_MOVE_JUMP | bits_CAP_MOVE_CLIMB);
+ // JAY: Disabled jump for now - hard to compare to HL1
+ CapabilitiesAdd( bits_CAP_TURN_HEAD | bits_CAP_MOVE_GROUND );
+
+ CapabilitiesAdd( bits_CAP_AIM_GUN );
+
+ // Innate range attack for grenade
+ // CapabilitiesAdd(bits_CAP_INNATE_RANGE_ATTACK2 );
+
+ // Innate range attack for kicking
+ CapabilitiesAdd(bits_CAP_INNATE_MELEE_ATTACK1 );
+
+ // Can be in a squad
+ CapabilitiesAdd( bits_CAP_SQUAD);
+ CapabilitiesAdd( bits_CAP_USE_WEAPONS );
+
+ CapabilitiesAdd( bits_CAP_DUCK ); // In reloading and cover
+
+ CapabilitiesAdd( bits_CAP_NO_HIT_SQUADMATES );
+
+ m_bFirstEncounter = true;// this is true when the grunt spawns, because he hasn't encountered an enemy yet.
+
+ m_HackedGunPos = Vector ( 0, 0, 55 );
+
+ m_flStopMoveShootTime = FLT_MAX; // Move and shoot defaults on.
+ m_MoveAndShootOverlay.SetInitialDelay( 0.75 ); // But with a bit of a delay.
+
+ m_flNextLostSoundTime = 0;
+ m_flAlertPatrolTime = 0;
+
+ m_flNextAltFireTime = gpGlobals->curtime;
+
+ NPCInit();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::CreateBehaviors()
+{
+ AddBehavior( &m_RappelBehavior );
+ AddBehavior( &m_ActBusyBehavior );
+ AddBehavior( &m_AssaultBehavior );
+ AddBehavior( &m_StandoffBehavior );
+ AddBehavior( &m_FollowBehavior );
+ AddBehavior( &m_FuncTankBehavior );
+
+ return BaseClass::CreateBehaviors();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_Combine::PostNPCInit()
+{
+ if( IsElite() )
+ {
+ // Give a warning if a Combine Soldier is equipped with anything other than
+ // an AR2.
+ if( !GetActiveWeapon() || !FClassnameIs( GetActiveWeapon(), "weapon_ar2" ) )
+ {
+ DevWarning("**Combine Elite Soldier MUST be equipped with AR2\n");
+ }
+ }
+
+ BaseClass::PostNPCInit();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_Combine::GatherConditions()
+{
+ BaseClass::GatherConditions();
+
+ ClearCondition( COND_COMBINE_ATTACK_SLOT_AVAILABLE );
+
+ if( GetState() == NPC_STATE_COMBAT )
+ {
+ if( IsCurSchedule( SCHED_COMBINE_WAIT_IN_COVER, false ) )
+ {
+ // Soldiers that are standing around doing nothing poll for attack slots so
+ // that they can respond quickly when one comes available. If they can
+ // occupy a vacant attack slot, they do so. This holds the slot until their
+ // schedule breaks and schedule selection runs again, essentially reserving this
+ // slot. If they do not select an attack schedule, then they'll release the slot.
+ if( OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ {
+ SetCondition( COND_COMBINE_ATTACK_SLOT_AVAILABLE );
+ }
+ }
+
+ if( IsUsingTacticalVariant(TACTICAL_VARIANT_PRESSURE_ENEMY_UNTIL_CLOSE) )
+ {
+ if( GetEnemy() != NULL && !HasCondition(COND_ENEMY_OCCLUDED) )
+ {
+ // Now we're close to our enemy, stop using the tactical variant.
+ if( GetAbsOrigin().DistToSqr(GetEnemy()->GetAbsOrigin()) < Square(30.0f * 12.0f) )
+ m_iTacticalVariant = TACTICAL_VARIANT_DEFAULT;
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CNPC_Combine::PrescheduleThink()
+{
+ BaseClass::PrescheduleThink();
+
+ // Speak any queued sentences
+ m_Sentences.UpdateSentenceQueue();
+
+ if ( IsOnFire() )
+ {
+ SetCondition( COND_COMBINE_ON_FIRE );
+ }
+ else
+ {
+ ClearCondition( COND_COMBINE_ON_FIRE );
+ }
+
+ extern ConVar ai_debug_shoot_positions;
+ if ( ai_debug_shoot_positions.GetBool() )
+ NDebugOverlay::Cross3D( EyePosition(), 16, 0, 255, 0, false, 0.1 );
+
+ if( gpGlobals->curtime >= m_flStopMoveShootTime )
+ {
+ // Time to stop move and shoot and start facing the way I'm running.
+ // This makes the combine look attentive when disengaging, but prevents
+ // them from always running around facing you.
+ //
+ // Only do this if it won't be immediately shut off again.
+ if( GetNavigator()->GetPathTimeToGoal() > 1.0f )
+ {
+ m_MoveAndShootOverlay.SuspendMoveAndShoot( 5.0f );
+ m_flStopMoveShootTime = FLT_MAX;
+ }
+ }
+
+ if( m_flGroundSpeed > 0 && GetState() == NPC_STATE_COMBAT && m_MoveAndShootOverlay.IsSuspended() )
+ {
+ // Return to move and shoot when near my goal so that I 'tuck into' the location facing my enemy.
+ if( GetNavigator()->GetPathTimeToGoal() <= 1.0f )
+ {
+ m_MoveAndShootOverlay.SuspendMoveAndShoot( 0 );
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_Combine::DelayAltFireAttack( float flDelay )
+{
+ float flNextAltFire = gpGlobals->curtime + flDelay;
+
+ if( flNextAltFire > m_flNextAltFireTime )
+ {
+ // Don't let this delay order preempt a previous request to wait longer.
+ m_flNextAltFireTime = flNextAltFire;
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_Combine::DelaySquadAltFireAttack( float flDelay )
+{
+ // Make sure to delay my own alt-fire attack.
+ DelayAltFireAttack( flDelay );
+
+ AISquadIter_t iter;
+ CAI_BaseNPC *pSquadmate = m_pSquad ? m_pSquad->GetFirstMember( &iter ) : NULL;
+ while ( pSquadmate )
+ {
+ CNPC_Combine *pCombine = dynamic_cast<CNPC_Combine*>(pSquadmate);
+
+ if( pCombine && pCombine->IsElite() )
+ {
+ pCombine->DelayAltFireAttack( flDelay );
+ }
+
+ pSquadmate = m_pSquad->GetNextMember( &iter );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: degrees to turn in 0.1 seconds
+//-----------------------------------------------------------------------------
+float CNPC_Combine::MaxYawSpeed( void )
+{
+ switch( GetActivity() )
+ {
+ case ACT_TURN_LEFT:
+ case ACT_TURN_RIGHT:
+ return 45;
+ break;
+ case ACT_RUN:
+ case ACT_RUN_HURT:
+ return 15;
+ break;
+ case ACT_WALK:
+ case ACT_WALK_CROUCH:
+ return 25;
+ break;
+ case ACT_RANGE_ATTACK1:
+ case ACT_RANGE_ATTACK2:
+ case ACT_MELEE_ATTACK1:
+ case ACT_MELEE_ATTACK2:
+ return 35;
+ default:
+ return 35;
+ break;
+ }
+}
+
+//-----------------------------------------------------------------------------
+//
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::ShouldMoveAndShoot()
+{
+ // Set this timer so that gpGlobals->curtime can't catch up to it.
+ // Essentially, we're saying that we're not going to interfere with
+ // what the AI wants to do with move and shoot.
+ //
+ // If any code below changes this timer, the code is saying
+ // "It's OK to move and shoot until gpGlobals->curtime == m_flStopMoveShootTime"
+ m_flStopMoveShootTime = FLT_MAX;
+
+ if( IsCurSchedule( SCHED_COMBINE_HIDE_AND_RELOAD, false ) )
+ m_flStopMoveShootTime = gpGlobals->curtime + random->RandomFloat( 0.4f, 0.6f );
+
+ if( IsCurSchedule( SCHED_TAKE_COVER_FROM_BEST_SOUND, false ) )
+ return false;
+
+ if( IsCurSchedule( SCHED_COMBINE_TAKE_COVER_FROM_BEST_SOUND, false ) )
+ return false;
+
+ if( IsCurSchedule( SCHED_COMBINE_RUN_AWAY_FROM_BEST_SOUND, false ) )
+ return false;
+
+ if( HasCondition( COND_NO_PRIMARY_AMMO, false ) )
+ m_flStopMoveShootTime = gpGlobals->curtime + random->RandomFloat( 0.4f, 0.6f );
+
+ if( m_pSquad && IsCurSchedule( SCHED_COMBINE_TAKE_COVER1, false ) )
+ m_flStopMoveShootTime = gpGlobals->curtime + random->RandomFloat( 0.4f, 0.6f );
+
+ return BaseClass::ShouldMoveAndShoot();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: turn in the direction of movement
+// Output :
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::OverrideMoveFacing( const AILocalMoveGoal_t &move, float flInterval )
+{
+ return BaseClass::OverrideMoveFacing( move, flInterval );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//
+//
+//-----------------------------------------------------------------------------
+Class_T CNPC_Combine::Classify ( void )
+{
+ return CLASS_COMBINE;
+}
+
+
+//-----------------------------------------------------------------------------
+// Continuous movement tasks
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::IsCurTaskContinuousMove()
+{
+ const Task_t* pTask = GetTask();
+ if ( pTask && (pTask->iTask == TASK_COMBINE_CHASE_ENEMY_CONTINUOUSLY) )
+ return true;
+
+ return BaseClass::IsCurTaskContinuousMove();
+}
+
+
+//-----------------------------------------------------------------------------
+// Chase the enemy, updating the target position as the player moves
+//-----------------------------------------------------------------------------
+void CNPC_Combine::StartTaskChaseEnemyContinuously( const Task_t *pTask )
+{
+ CBaseEntity *pEnemy = GetEnemy();
+ if ( !pEnemy )
+ {
+ TaskFail( FAIL_NO_ENEMY );
+ return;
+ }
+
+ // We're done once we get close enough
+ if ( WorldSpaceCenter().DistToSqr( pEnemy->WorldSpaceCenter() ) <= pTask->flTaskData * pTask->flTaskData )
+ {
+ TaskComplete();
+ return;
+ }
+
+ // TASK_GET_PATH_TO_ENEMY
+ if ( IsUnreachable( pEnemy ) )
+ {
+ TaskFail(FAIL_NO_ROUTE);
+ return;
+ }
+
+ if ( !GetNavigator()->SetGoal( GOALTYPE_ENEMY, AIN_NO_PATH_TASK_FAIL ) )
+ {
+ // no way to get there =(
+ DevWarning( 2, "GetPathToEnemy failed!!\n" );
+ RememberUnreachable( pEnemy );
+ TaskFail(FAIL_NO_ROUTE);
+ return;
+ }
+
+ // NOTE: This is TaskRunPath here.
+ if ( TranslateActivity( ACT_RUN ) != ACT_INVALID )
+ {
+ GetNavigator()->SetMovementActivity( ACT_RUN );
+ }
+ else
+ {
+ GetNavigator()->SetMovementActivity(ACT_WALK);
+ }
+
+ // Cover is void once I move
+ Forget( bits_MEMORY_INCOVER );
+
+ if (GetNavigator()->GetGoalType() == GOALTYPE_NONE)
+ {
+ TaskComplete();
+ GetNavigator()->ClearGoal(); // Clear residual state
+ return;
+ }
+
+ // No shooting delay when in this mode
+ m_MoveAndShootOverlay.SetInitialDelay( 0.0 );
+
+ if (!GetNavigator()->IsGoalActive())
+ {
+ SetIdealActivity( GetStoppedActivity() );
+ }
+ else
+ {
+ // Check validity of goal type
+ ValidateNavGoal();
+ }
+
+ // set that we're probably going to stop before the goal
+ GetNavigator()->SetArrivalDistance( pTask->flTaskData );
+ m_vSavePosition = GetEnemy()->WorldSpaceCenter();
+}
+
+void CNPC_Combine::RunTaskChaseEnemyContinuously( const Task_t *pTask )
+{
+ if (!GetNavigator()->IsGoalActive())
+ {
+ SetIdealActivity( GetStoppedActivity() );
+ }
+ else
+ {
+ // Check validity of goal type
+ ValidateNavGoal();
+ }
+
+ CBaseEntity *pEnemy = GetEnemy();
+ if ( !pEnemy )
+ {
+ TaskFail( FAIL_NO_ENEMY );
+ return;
+ }
+
+ // We're done once we get close enough
+ if ( WorldSpaceCenter().DistToSqr( pEnemy->WorldSpaceCenter() ) <= pTask->flTaskData * pTask->flTaskData )
+ {
+ GetNavigator()->StopMoving();
+ TaskComplete();
+ return;
+ }
+
+ // Recompute path if the enemy has moved too much
+ if ( m_vSavePosition.DistToSqr( pEnemy->WorldSpaceCenter() ) < (pTask->flTaskData * pTask->flTaskData) )
+ return;
+
+ if ( IsUnreachable( pEnemy ) )
+ {
+ TaskFail(FAIL_NO_ROUTE);
+ return;
+ }
+
+ if ( !GetNavigator()->RefindPathToGoal() )
+ {
+ TaskFail(FAIL_NO_ROUTE);
+ return;
+ }
+
+ m_vSavePosition = pEnemy->WorldSpaceCenter();
+}
+
+
+//=========================================================
+// start task
+//=========================================================
+void CNPC_Combine::StartTask( const Task_t *pTask )
+{
+ // NOTE: This reset is required because we change it in TASK_COMBINE_CHASE_ENEMY_CONTINUOUSLY
+ m_MoveAndShootOverlay.SetInitialDelay( 0.75 );
+
+ switch ( pTask->iTask )
+ {
+ case TASK_COMBINE_SET_STANDING:
+ {
+ if ( pTask->flTaskData == 1.0f)
+ {
+ Stand();
+ }
+ else
+ {
+ Crouch();
+ }
+ TaskComplete();
+ }
+ break;
+
+ case TASK_COMBINE_CHASE_ENEMY_CONTINUOUSLY:
+ StartTaskChaseEnemyContinuously( pTask );
+ break;
+
+ case TASK_COMBINE_PLAY_SEQUENCE_FACE_ALTFIRE_TARGET:
+ SetIdealActivity( (Activity)(int)pTask->flTaskData );
+ GetMotor()->SetIdealYawToTargetAndUpdate( m_vecAltFireTarget, AI_KEEP_YAW_SPEED );
+ break;
+
+ case TASK_COMBINE_SIGNAL_BEST_SOUND:
+ if( IsInSquad() && GetSquad()->NumMembers() > 1 )
+ {
+ CBasePlayer *pPlayer = AI_GetSinglePlayer();
+
+ if( pPlayer && OccupyStrategySlot( SQUAD_SLOT_EXCLUSIVE_HANDSIGN ) && pPlayer->FInViewCone( this ) )
+ {
+ CSound *pSound;
+ pSound = GetBestSound();
+
+ Assert( pSound != NULL );
+
+ if ( pSound )
+ {
+ Vector right, tosound;
+
+ GetVectors( NULL, &right, NULL );
+
+ tosound = pSound->GetSoundReactOrigin() - GetAbsOrigin();
+ VectorNormalize( tosound);
+
+ tosound.z = 0;
+ right.z = 0;
+
+ if( DotProduct( right, tosound ) > 0 )
+ {
+ // Right
+ SetIdealActivity( ACT_SIGNAL_RIGHT );
+ }
+ else
+ {
+ // Left
+ SetIdealActivity( ACT_SIGNAL_LEFT );
+ }
+
+ break;
+ }
+ }
+ }
+
+ // Otherwise, just skip it.
+ TaskComplete();
+ break;
+
+ case TASK_ANNOUNCE_ATTACK:
+ {
+ // If Primary Attack
+ if ((int)pTask->flTaskData == 1)
+ {
+ // -----------------------------------------------------------
+ // If enemy isn't facing me and I haven't attacked in a while
+ // annouce my attack before I start wailing away
+ // -----------------------------------------------------------
+ CBaseCombatCharacter *pBCC = GetEnemyCombatCharacterPointer();
+
+ if (pBCC && pBCC->IsPlayer() && (!pBCC->FInViewCone ( this )) &&
+ (gpGlobals->curtime - m_flLastAttackTime > 3.0) )
+ {
+ m_flLastAttackTime = gpGlobals->curtime;
+
+ m_Sentences.Speak( "COMBINE_ANNOUNCE", SENTENCE_PRIORITY_HIGH );
+
+ // Wait two seconds
+ SetWait( 2.0 );
+
+ if ( !IsCrouching() )
+ {
+ SetActivity(ACT_IDLE);
+ }
+ else
+ {
+ SetActivity(ACT_COWER); // This is really crouch idle
+ }
+ }
+ // -------------------------------------------------------------
+ // Otherwise move on
+ // -------------------------------------------------------------
+ else
+ {
+ TaskComplete();
+ }
+ }
+ else
+ {
+ m_Sentences.Speak( "COMBINE_THROW_GRENADE", SENTENCE_PRIORITY_MEDIUM );
+ SetActivity(ACT_IDLE);
+
+ // Wait two seconds
+ SetWait( 2.0 );
+ }
+ break;
+ }
+
+ case TASK_WALK_PATH:
+ case TASK_RUN_PATH:
+ // grunt no longer assumes he is covered if he moves
+ Forget( bits_MEMORY_INCOVER );
+ BaseClass::StartTask( pTask );
+ break;
+
+ case TASK_COMBINE_FACE_TOSS_DIR:
+ break;
+
+ case TASK_COMBINE_GET_PATH_TO_FORCED_GREN_LOS:
+ {
+ if ( !m_hForcedGrenadeTarget )
+ {
+ TaskFail(FAIL_NO_ENEMY);
+ return;
+ }
+
+ float flMaxRange = 2000;
+ float flMinRange = 0;
+
+ Vector vecEnemy = m_hForcedGrenadeTarget->GetAbsOrigin();
+ Vector vecEnemyEye = vecEnemy + m_hForcedGrenadeTarget->GetViewOffset();
+
+ Vector posLos;
+ bool found = false;
+
+ if ( GetTacticalServices()->FindLateralLos( vecEnemyEye, &posLos ) )
+ {
+ float dist = ( posLos - vecEnemyEye ).Length();
+ if ( dist < flMaxRange && dist > flMinRange )
+ found = true;
+ }
+
+ if ( !found && GetTacticalServices()->FindLos( vecEnemy, vecEnemyEye, flMinRange, flMaxRange, 1.0, &posLos ) )
+ {
+ found = true;
+ }
+
+ if ( !found )
+ {
+ TaskFail( FAIL_NO_SHOOT );
+ }
+ else
+ {
+ // else drop into run task to offer an interrupt
+ m_vInterruptSavePosition = posLos;
+ }
+ }
+ break;
+
+ case TASK_COMBINE_IGNORE_ATTACKS:
+ // must be in a squad
+ if (m_pSquad && m_pSquad->NumMembers() > 2)
+ {
+ // the enemy must be far enough away
+ if (GetEnemy() && (GetEnemy()->WorldSpaceCenter() - WorldSpaceCenter()).Length() > 512.0 )
+ {
+ m_flNextAttack = gpGlobals->curtime + pTask->flTaskData;
+ }
+ }
+ TaskComplete( );
+ break;
+
+ case TASK_COMBINE_DEFER_SQUAD_GRENADES:
+ {
+ if ( m_pSquad )
+ {
+ // iterate my squad and stop everyone from throwing grenades for a little while.
+ AISquadIter_t iter;
+
+ CAI_BaseNPC *pSquadmate = m_pSquad ? m_pSquad->GetFirstMember( &iter ) : NULL;
+ while ( pSquadmate )
+ {
+ CNPC_Combine *pCombine = dynamic_cast<CNPC_Combine*>(pSquadmate);
+
+ if( pCombine )
+ {
+ pCombine->m_flNextGrenadeCheck = gpGlobals->curtime + 5;
+ }
+
+ pSquadmate = m_pSquad->GetNextMember( &iter );
+ }
+ }
+
+ TaskComplete();
+ break;
+ }
+
+ case TASK_FACE_IDEAL:
+ case TASK_FACE_ENEMY:
+ {
+ if( pTask->iTask == TASK_FACE_ENEMY && HasCondition( COND_CAN_RANGE_ATTACK1 ) )
+ {
+ TaskComplete();
+ return;
+ }
+
+ BaseClass::StartTask( pTask );
+ bool bIsFlying = (GetMoveType() == MOVETYPE_FLY) || (GetMoveType() == MOVETYPE_FLYGRAVITY);
+ if (bIsFlying)
+ {
+ SetIdealActivity( ACT_GLIDE );
+ }
+
+ }
+ break;
+
+ case TASK_FIND_COVER_FROM_ENEMY:
+ {
+ if (GetHintGroup() == NULL_STRING)
+ {
+ CBaseEntity *pEntity = GetEnemy();
+
+ // FIXME: this should be generalized by the schedules that are selected, or in the definition of
+ // what "cover" means (i.e., trace attack vulnerability vs. physical attack vulnerability
+ if ( pEntity )
+ {
+ // NOTE: This is a good time to check to see if the player is hurt.
+ // Have the combine notice this and call out
+ if ( !HasMemory(bits_MEMORY_PLAYER_HURT) && pEntity->IsPlayer() && pEntity->GetHealth() <= 20 )
+ {
+ if ( m_pSquad )
+ {
+ m_pSquad->SquadRemember(bits_MEMORY_PLAYER_HURT);
+ }
+
+ m_Sentences.Speak( "COMBINE_PLAYERHIT", SENTENCE_PRIORITY_INVALID );
+ JustMadeSound( SENTENCE_PRIORITY_HIGH );
+ }
+ if ( pEntity->MyNPCPointer() )
+ {
+ if ( !(pEntity->MyNPCPointer()->CapabilitiesGet( ) & bits_CAP_WEAPON_RANGE_ATTACK1) &&
+ !(pEntity->MyNPCPointer()->CapabilitiesGet( ) & bits_CAP_INNATE_RANGE_ATTACK1) )
+ {
+ TaskComplete();
+ return;
+ }
+ }
+ }
+ }
+ BaseClass::StartTask( pTask );
+ }
+ break;
+ case TASK_RANGE_ATTACK1:
+ {
+ m_nShots = GetActiveWeapon()->GetRandomBurst();
+ m_flShotDelay = GetActiveWeapon()->GetFireRate();
+
+ m_flNextAttack = gpGlobals->curtime + m_flShotDelay - 0.1;
+ ResetIdealActivity( ACT_RANGE_ATTACK1 );
+ m_flLastAttackTime = gpGlobals->curtime;
+ }
+ break;
+
+ case TASK_COMBINE_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;
+
+ default:
+ BaseClass:: StartTask( pTask );
+ break;
+ }
+}
+
+//=========================================================
+// RunTask
+//=========================================================
+void CNPC_Combine::RunTask( const Task_t *pTask )
+{
+ /*
+ {
+ CBaseEntity *pEnemy = GetEnemy();
+ if (pEnemy)
+ {
+ NDebugOverlay::Line(Center(), pEnemy->Center(), 0,255,255, false, 0.1);
+ }
+
+ }
+ */
+
+ /*
+ if (m_iMySquadSlot != SQUAD_SLOT_NONE)
+ {
+ char text[64];
+ Q_snprintf( text, strlen( text ), "%d", m_iMySquadSlot );
+
+ NDebugOverlay::Text( Center() + Vector( 0, 0, 72 ), text, false, 0.1 );
+ }
+ */
+
+ switch ( pTask->iTask )
+ {
+ case TASK_COMBINE_CHASE_ENEMY_CONTINUOUSLY:
+ RunTaskChaseEnemyContinuously( pTask );
+ break;
+
+ case TASK_COMBINE_SIGNAL_BEST_SOUND:
+ AutoMovement( );
+ if ( IsActivityFinished() )
+ {
+ TaskComplete();
+ }
+ break;
+
+ case TASK_ANNOUNCE_ATTACK:
+ {
+ // Stop waiting if enemy facing me or lost enemy
+ CBaseCombatCharacter* pBCC = GetEnemyCombatCharacterPointer();
+ if (!pBCC || pBCC->FInViewCone( this ))
+ {
+ TaskComplete();
+ }
+
+ if ( IsWaitFinished() )
+ {
+ TaskComplete();
+ }
+ }
+ break;
+
+ case TASK_COMBINE_PLAY_SEQUENCE_FACE_ALTFIRE_TARGET:
+ GetMotor()->SetIdealYawToTargetAndUpdate( m_vecAltFireTarget, AI_KEEP_YAW_SPEED );
+
+ if ( IsActivityFinished() )
+ {
+ TaskComplete();
+ }
+ break;
+
+ case TASK_COMBINE_FACE_TOSS_DIR:
+ {
+ // project a point along the toss vector and turn to face that point.
+ GetMotor()->SetIdealYawToTargetAndUpdate( GetLocalOrigin() + m_vecTossVelocity * 64, AI_KEEP_YAW_SPEED );
+
+ if ( FacingIdeal() )
+ {
+ TaskComplete( true );
+ }
+ break;
+ }
+
+ case TASK_COMBINE_GET_PATH_TO_FORCED_GREN_LOS:
+ {
+ if ( !m_hForcedGrenadeTarget )
+ {
+ TaskFail(FAIL_NO_ENEMY);
+ return;
+ }
+
+ if ( GetTaskInterrupt() > 0 )
+ {
+ ClearTaskInterrupt();
+
+ Vector vecEnemy = m_hForcedGrenadeTarget->GetAbsOrigin();
+ AI_NavGoal_t goal( m_vInterruptSavePosition, ACT_RUN, AIN_HULL_TOLERANCE );
+
+ GetNavigator()->SetGoal( goal, AIN_CLEAR_TARGET );
+ GetNavigator()->SetArrivalDirection( vecEnemy - goal.dest );
+ }
+ else
+ {
+ TaskInterrupt();
+ }
+ }
+ break;
+
+ case TASK_RANGE_ATTACK1:
+ {
+ AutoMovement( );
+
+ Vector vecEnemyLKP = GetEnemyLKP();
+ if (!FInAimCone( vecEnemyLKP ))
+ {
+ GetMotor()->SetIdealYawToTargetAndUpdate( vecEnemyLKP, AI_KEEP_YAW_SPEED );
+ }
+ else
+ {
+ GetMotor()->SetIdealYawAndUpdate( GetMotor()->GetIdealYaw(), AI_KEEP_YAW_SPEED );
+ }
+
+ if ( gpGlobals->curtime >= m_flNextAttack )
+ {
+ if ( IsActivityFinished() )
+ {
+ if (--m_nShots > 0)
+ {
+ // DevMsg("ACT_RANGE_ATTACK1\n");
+ ResetIdealActivity( ACT_RANGE_ATTACK1 );
+ m_flLastAttackTime = gpGlobals->curtime;
+ m_flNextAttack = gpGlobals->curtime + m_flShotDelay - 0.1;
+ }
+ else
+ {
+ // DevMsg("TASK_RANGE_ATTACK1 complete\n");
+ TaskComplete();
+ }
+ }
+ }
+ else
+ {
+ // DevMsg("Wait\n");
+ }
+ }
+ break;
+
+ default:
+ {
+ BaseClass::RunTask( pTask );
+ break;
+ }
+ }
+}
+
+//------------------------------------------------------------------------------
+// Purpose : Override to always shoot at eyes (for ducking behind things)
+// Input :
+// Output :
+//------------------------------------------------------------------------------
+Vector CNPC_Combine::BodyTarget( const Vector &posSrc, bool bNoisy )
+{
+ Vector result = BaseClass::BodyTarget( posSrc, bNoisy );
+
+ // @TODO (toml 02-02-04): this seems wrong. Isn't this already be accounted for
+ // with the eye position used in the base BodyTarget()
+ if ( GetFlags() & FL_DUCKING )
+ result -= Vector(0,0,24);
+
+ return result;
+}
+
+//------------------------------------------------------------------------------
+// Purpose:
+//------------------------------------------------------------------------------
+bool CNPC_Combine::FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker )
+{
+ if( m_spawnflags & SF_COMBINE_NO_LOOK )
+ {
+ // When no look is set, if enemy has eluded the squad,
+ // he's always invisble to me
+ if (GetEnemies()->HasEludedMe(pEntity))
+ {
+ return false;
+ }
+ }
+ return BaseClass::FVisible(pEntity, traceMask, ppBlocker);
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_Combine::Event_Killed( const CTakeDamageInfo &info )
+{
+ // if I was killed before I could finish throwing my grenade, drop
+ // a grenade item that the player can retrieve.
+ if( GetActivity() == ACT_RANGE_ATTACK2 )
+ {
+ if( m_iLastAnimEventHandled != COMBINE_AE_GREN_TOSS )
+ {
+ // Drop the grenade as an item.
+ Vector vecStart;
+ GetAttachment( "lefthand", vecStart );
+
+ CBaseEntity *pItem = DropItem( "weapon_frag", vecStart, RandomAngle(0,360) );
+
+ if ( pItem )
+ {
+ IPhysicsObject *pObj = pItem->VPhysicsGetObject();
+
+ if ( pObj )
+ {
+ Vector vel;
+ vel.x = random->RandomFloat( -100.0f, 100.0f );
+ vel.y = random->RandomFloat( -100.0f, 100.0f );
+ vel.z = random->RandomFloat( 800.0f, 1200.0f );
+ AngularImpulse angImp = RandomAngularImpulse( -300.0f, 300.0f );
+
+ vel[2] = 0.0f;
+ pObj->AddVelocity( &vel, &angImp );
+ }
+
+ // In the Citadel we need to dissolve this
+ if ( PlayerHasMegaPhysCannon() )
+ {
+ CBaseCombatWeapon *pWeapon = static_cast<CBaseCombatWeapon *>(pItem);
+
+ pWeapon->Dissolve( NULL, gpGlobals->curtime, false, ENTITY_DISSOLVE_NORMAL );
+ }
+ }
+ }
+ }
+
+ BaseClass::Event_Killed( info );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Override. Don't update if I'm not looking
+// Input :
+// Output : Returns true is new enemy, false is known enemy
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::UpdateEnemyMemory( CBaseEntity *pEnemy, const Vector &position, CBaseEntity *pInformer )
+{
+ if( m_spawnflags & SF_COMBINE_NO_LOOK )
+ {
+ return false;
+ }
+
+ return BaseClass::UpdateEnemyMemory( pEnemy, position, pInformer );
+}
+
+
+//-----------------------------------------------------------------------------
+// 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_Combine::BuildScheduleTestBits( void )
+{
+ BaseClass::BuildScheduleTestBits();
+
+ if (gpGlobals->curtime < m_flNextAttack)
+ {
+ ClearCustomInterruptCondition( COND_CAN_RANGE_ATTACK1 );
+ ClearCustomInterruptCondition( COND_CAN_RANGE_ATTACK2 );
+ }
+
+ SetCustomInterruptCondition( COND_COMBINE_HIT_BY_BUGBAIT );
+
+ if ( !IsCurSchedule( SCHED_COMBINE_BURNING_STAND ) )
+ {
+ SetCustomInterruptCondition( COND_COMBINE_ON_FIRE );
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Translate base class activities into combot activites
+//-----------------------------------------------------------------------------
+Activity CNPC_Combine::NPC_TranslateActivity( Activity eNewActivity )
+{
+ //Slaming this back to ACT_COMBINE_BUGBAIT since we don't want ANYTHING to change our activity while we burn.
+ if ( HasCondition( COND_COMBINE_ON_FIRE ) )
+ return BaseClass::NPC_TranslateActivity( ACT_COMBINE_BUGBAIT );
+
+ if (eNewActivity == ACT_RANGE_ATTACK2)
+ {
+ // grunt is going to a secondary long range attack. This may be a thrown
+ // grenade or fired grenade, we must determine which and pick proper sequence
+ if (Weapon_OwnsThisType( "weapon_grenadelauncher" ) )
+ {
+ return ( Activity )ACT_COMBINE_LAUNCH_GRENADE;
+ }
+ else
+ {
+ return ( Activity )ACT_COMBINE_THROW_GRENADE;
+ }
+ }
+ else if (eNewActivity == ACT_IDLE)
+ {
+ if ( !IsCrouching() && ( m_NPCState == NPC_STATE_COMBAT || m_NPCState == NPC_STATE_ALERT ) )
+ {
+ eNewActivity = ACT_IDLE_ANGRY;
+ }
+ }
+
+ if ( m_AssaultBehavior.IsRunning() )
+ {
+ switch ( eNewActivity )
+ {
+ case ACT_IDLE:
+ eNewActivity = ACT_IDLE_ANGRY;
+ break;
+
+ case ACT_WALK:
+ eNewActivity = ACT_WALK_AIM;
+ break;
+
+ case ACT_RUN:
+ eNewActivity = ACT_RUN_AIM;
+ break;
+ }
+ }
+
+ return BaseClass::NPC_TranslateActivity( eNewActivity );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Overidden for human grunts because they hear the DANGER sound
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+int CNPC_Combine::GetSoundInterests( void )
+{
+ return SOUND_WORLD | SOUND_COMBAT | SOUND_PLAYER | SOUND_DANGER | SOUND_PHYSICS_DANGER | SOUND_BULLET_IMPACT | SOUND_MOVE_AWAY;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Return true if this NPC can hear the specified sound
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::QueryHearSound( CSound *pSound )
+{
+ if ( pSound->SoundContext() & SOUND_CONTEXT_COMBINE_ONLY )
+ return true;
+
+ if ( pSound->SoundContext() & SOUND_CONTEXT_EXCLUDE_COMBINE )
+ return false;
+
+ return BaseClass::QueryHearSound( pSound );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Announce an assault if the enemy can see me and we are pretty
+// close to him/her
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+void CNPC_Combine::AnnounceAssault(void)
+{
+ if (random->RandomInt(0,5) > 1)
+ return;
+
+ // If enemy can see me make assualt sound
+ CBaseCombatCharacter* pBCC = GetEnemyCombatCharacterPointer();
+
+ if (!pBCC)
+ return;
+
+ if (!FOkToMakeSound())
+ return;
+
+ // Make sure we are pretty close
+ if ( WorldSpaceCenter().DistToSqr( pBCC->WorldSpaceCenter() ) > (2000 * 2000))
+ return;
+
+ // Make sure we are in view cone of player
+ if (!pBCC->FInViewCone ( this ))
+ return;
+
+ // Make sure player can see me
+ if ( FVisible( pBCC ) )
+ {
+ m_Sentences.Speak( "COMBINE_ASSAULT" );
+ }
+}
+
+
+void CNPC_Combine::AnnounceEnemyType( CBaseEntity *pEnemy )
+{
+ const char *pSentenceName = "COMBINE_MONST";
+ switch ( pEnemy->Classify() )
+ {
+ case CLASS_PLAYER:
+ pSentenceName = "COMBINE_ALERT";
+ break;
+
+ case CLASS_PLAYER_ALLY:
+ case CLASS_CITIZEN_REBEL:
+ case CLASS_CITIZEN_PASSIVE:
+ case CLASS_VORTIGAUNT:
+ pSentenceName = "COMBINE_MONST_CITIZENS";
+ break;
+
+ case CLASS_PLAYER_ALLY_VITAL:
+ pSentenceName = "COMBINE_MONST_CHARACTER";
+ break;
+
+ case CLASS_ANTLION:
+ pSentenceName = "COMBINE_MONST_BUGS";
+ break;
+
+ case CLASS_ZOMBIE:
+ pSentenceName = "COMBINE_MONST_ZOMBIES";
+ break;
+
+ case CLASS_HEADCRAB:
+ case CLASS_BARNACLE:
+ pSentenceName = "COMBINE_MONST_PARASITES";
+ break;
+ }
+
+ m_Sentences.Speak( pSentenceName, SENTENCE_PRIORITY_HIGH );
+}
+
+void CNPC_Combine::AnnounceEnemyKill( CBaseEntity *pEnemy )
+{
+ if (!pEnemy )
+ return;
+
+ const char *pSentenceName = "COMBINE_KILL_MONST";
+ switch ( pEnemy->Classify() )
+ {
+ case CLASS_PLAYER:
+ pSentenceName = "COMBINE_PLAYER_DEAD";
+ break;
+
+ // no sentences for these guys yet
+ case CLASS_PLAYER_ALLY:
+ case CLASS_CITIZEN_REBEL:
+ case CLASS_CITIZEN_PASSIVE:
+ case CLASS_VORTIGAUNT:
+ break;
+
+ case CLASS_PLAYER_ALLY_VITAL:
+ break;
+
+ case CLASS_ANTLION:
+ break;
+
+ case CLASS_ZOMBIE:
+ break;
+
+ case CLASS_HEADCRAB:
+ case CLASS_BARNACLE:
+ break;
+ }
+
+ m_Sentences.Speak( pSentenceName, SENTENCE_PRIORITY_HIGH );
+}
+
+//-----------------------------------------------------------------------------
+// Select the combat schedule
+//-----------------------------------------------------------------------------
+int CNPC_Combine::SelectCombatSchedule()
+{
+ // -----------
+ // dead enemy
+ // -----------
+ if ( HasCondition( COND_ENEMY_DEAD ) )
+ {
+ // call base class, all code to handle dead enemies is centralized there.
+ return SCHED_NONE;
+ }
+
+ // -----------
+ // new enemy
+ // -----------
+ if ( HasCondition( COND_NEW_ENEMY ) )
+ {
+ CBaseEntity *pEnemy = GetEnemy();
+ bool bFirstContact = false;
+ float flTimeSinceFirstSeen = gpGlobals->curtime - GetEnemies()->FirstTimeSeen( pEnemy );
+
+ if( flTimeSinceFirstSeen < 3.0f )
+ bFirstContact = true;
+
+ if ( m_pSquad && pEnemy )
+ {
+ if ( HasCondition( COND_SEE_ENEMY ) )
+ {
+ AnnounceEnemyType( pEnemy );
+ }
+
+ if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) && OccupyStrategySlot( SQUAD_SLOT_ATTACK1 ) )
+ {
+ // Start suppressing if someone isn't firing already (SLOT_ATTACK1). This means
+ // I'm the guy who spotted the enemy, I should react immediately.
+ return SCHED_COMBINE_SUPPRESS;
+ }
+
+ if ( m_pSquad->IsLeader( this ) || ( m_pSquad->GetLeader() && m_pSquad->GetLeader()->GetEnemy() != pEnemy ) )
+ {
+ // I'm the leader, but I didn't get the job suppressing the enemy. We know this because
+ // This code only runs if the code above didn't assign me SCHED_COMBINE_SUPPRESS.
+ if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) && OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ {
+ return SCHED_RANGE_ATTACK1;
+ }
+
+ if( HasCondition(COND_WEAPON_HAS_LOS) && IsStrategySlotRangeOccupied( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ {
+ // If everyone else is attacking and I have line of fire, wait for a chance to cover someone.
+ if( OccupyStrategySlot( SQUAD_SLOT_OVERWATCH ) )
+ {
+ return SCHED_COMBINE_ENTER_OVERWATCH;
+ }
+ }
+ }
+ else
+ {
+ if ( m_pSquad->GetLeader() && FOkToMakeSound( SENTENCE_PRIORITY_MEDIUM ) )
+ {
+ JustMadeSound( SENTENCE_PRIORITY_MEDIUM ); // squelch anything that isn't high priority so the leader can speak
+ }
+
+ // First contact, and I'm solo, or not the squad leader.
+ if( HasCondition( COND_SEE_ENEMY ) && CanGrenadeEnemy() )
+ {
+ if( OccupyStrategySlot( SQUAD_SLOT_GRENADE1 ) )
+ {
+ return SCHED_RANGE_ATTACK2;
+ }
+ }
+
+ if( !bFirstContact && OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ {
+ if( random->RandomInt(0, 100) < 60 )
+ {
+ return SCHED_ESTABLISH_LINE_OF_FIRE;
+ }
+ else
+ {
+ return SCHED_COMBINE_PRESS_ATTACK;
+ }
+ }
+
+ return SCHED_TAKE_COVER_FROM_ENEMY;
+ }
+ }
+ }
+
+ // ---------------------
+ // no ammo
+ // ---------------------
+ if ( ( HasCondition ( COND_NO_PRIMARY_AMMO ) || HasCondition ( COND_LOW_PRIMARY_AMMO ) ) && !HasCondition( COND_CAN_MELEE_ATTACK1) )
+ {
+ return SCHED_HIDE_AND_RELOAD;
+ }
+
+ // ----------------------
+ // LIGHT DAMAGE
+ // ----------------------
+ if ( HasCondition( COND_LIGHT_DAMAGE ) )
+ {
+ if ( GetEnemy() != NULL )
+ {
+ // only try to take cover if we actually have an enemy!
+
+ // FIXME: need to take cover for enemy dealing the damage
+
+ // A standing guy will either crouch or run.
+ // A crouching guy tries to stay stuck in.
+ if( !IsCrouching() )
+ {
+ if( GetEnemy() && random->RandomFloat( 0, 100 ) < 50 && CouldShootIfCrouching( GetEnemy() ) )
+ {
+ Crouch();
+ }
+ else
+ {
+ //!!!KELLY - this grunt was hit and is going to run to cover.
+ // m_Sentences.Speak( "COMBINE_COVER" );
+ return SCHED_TAKE_COVER_FROM_ENEMY;
+ }
+ }
+ }
+ else
+ {
+ // How am I wounded in combat with no enemy?
+ Assert( GetEnemy() != NULL );
+ }
+ }
+
+ // If I'm scared of this enemy run away
+ if ( IRelationType( GetEnemy() ) == D_FR )
+ {
+ if (HasCondition( COND_SEE_ENEMY ) ||
+ HasCondition( COND_SEE_FEAR ) ||
+ HasCondition( COND_LIGHT_DAMAGE ) ||
+ HasCondition( COND_HEAVY_DAMAGE ))
+ {
+ FearSound();
+ //ClearCommandGoal();
+ return SCHED_RUN_FROM_ENEMY;
+ }
+
+ // If I've seen the enemy recently, cower. Ignore the time for unforgettable enemies.
+ AI_EnemyInfo_t *pMemory = GetEnemies()->Find( GetEnemy() );
+ if ( (pMemory && pMemory->bUnforgettable) || (GetEnemyLastTimeSeen() > (gpGlobals->curtime - 5.0)) )
+ {
+ // If we're facing him, just look ready. Otherwise, face him.
+ if ( FInAimCone( GetEnemy()->EyePosition() ) )
+ return SCHED_COMBAT_STAND;
+
+ return SCHED_FEAR_FACE;
+ }
+ }
+
+ int attackSchedule = SelectScheduleAttack();
+ if ( attackSchedule != SCHED_NONE )
+ return attackSchedule;
+
+ if (HasCondition(COND_ENEMY_OCCLUDED))
+ {
+ // stand up, just in case
+ Stand();
+ DesireStand();
+
+ if( GetEnemy() && !(GetEnemy()->GetFlags() & FL_NOTARGET) && OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ {
+ // Charge in and break the enemy's cover!
+ return SCHED_ESTABLISH_LINE_OF_FIRE;
+ }
+
+ // If I'm a long, long way away, establish a LOF anyway. Once I get there I'll
+ // start respecting the squad slots again.
+ float flDistSq = GetEnemy()->WorldSpaceCenter().DistToSqr( WorldSpaceCenter() );
+ if ( flDistSq > Square(3000) )
+ return SCHED_ESTABLISH_LINE_OF_FIRE;
+
+ // Otherwise tuck in.
+ Remember( bits_MEMORY_INCOVER );
+ return SCHED_COMBINE_WAIT_IN_COVER;
+ }
+
+ // --------------------------------------------------------------
+ // Enemy not occluded but isn't open to attack
+ // --------------------------------------------------------------
+ if ( HasCondition( COND_SEE_ENEMY ) && !HasCondition( COND_CAN_RANGE_ATTACK1 ) )
+ {
+ if ( (HasCondition( COND_TOO_FAR_TO_ATTACK ) || IsUsingTacticalVariant(TACTICAL_VARIANT_PRESSURE_ENEMY) ) && OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ))
+ {
+ return SCHED_COMBINE_PRESS_ATTACK;
+ }
+
+ AnnounceAssault();
+ return SCHED_COMBINE_ASSAULT;
+ }
+
+ return SCHED_NONE;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+int CNPC_Combine::SelectSchedule( void )
+{
+ if ( IsWaitingToRappel() && BehaviorSelectSchedule() )
+ {
+ return BaseClass::SelectSchedule();
+ }
+
+ if ( HasCondition(COND_COMBINE_ON_FIRE) )
+ return SCHED_COMBINE_BURNING_STAND;
+
+ int nSched = SelectFlinchSchedule();
+ if ( nSched != SCHED_NONE )
+ return nSched;
+
+ if ( m_hForcedGrenadeTarget )
+ {
+ if ( m_flNextGrenadeCheck < gpGlobals->curtime )
+ {
+ Vector vecTarget = m_hForcedGrenadeTarget->WorldSpaceCenter();
+
+ if ( IsElite() )
+ {
+ if ( FVisible( m_hForcedGrenadeTarget ) )
+ {
+ m_vecAltFireTarget = vecTarget;
+ m_hForcedGrenadeTarget = NULL;
+ return SCHED_COMBINE_AR2_ALTFIRE;
+ }
+ }
+ else
+ {
+ // If we can, throw a grenade at the target.
+ // Ignore grenade count / distance / etc
+ if ( CheckCanThrowGrenade( vecTarget ) )
+ {
+ m_hForcedGrenadeTarget = NULL;
+ return SCHED_COMBINE_FORCED_GRENADE_THROW;
+ }
+ }
+ }
+
+ // Can't throw at the target, so lets try moving to somewhere where I can see it
+ if ( !FVisible( m_hForcedGrenadeTarget ) )
+ {
+ return SCHED_COMBINE_MOVE_TO_FORCED_GREN_LOS;
+ }
+ }
+
+ if ( m_NPCState != NPC_STATE_SCRIPT)
+ {
+ // If we're hit by bugbait, thrash around
+ if ( HasCondition( COND_COMBINE_HIT_BY_BUGBAIT ) )
+ {
+ // Don't do this if we're mounting a func_tank
+ if ( m_FuncTankBehavior.IsMounted() == true )
+ {
+ m_FuncTankBehavior.Dismount();
+ }
+
+ ClearCondition( COND_COMBINE_HIT_BY_BUGBAIT );
+ return SCHED_COMBINE_BUGBAIT_DISTRACTION;
+ }
+
+ // We've been told to move away from a target to make room for a grenade to be thrown at it
+ if ( HasCondition( COND_HEAR_MOVE_AWAY ) )
+ {
+ return SCHED_MOVE_AWAY;
+ }
+
+ // These things are done in any state but dead and prone
+ if (m_NPCState != NPC_STATE_DEAD && m_NPCState != NPC_STATE_PRONE )
+ {
+ // Cower when physics objects are thrown at me
+ if ( HasCondition( COND_HEAR_PHYSICS_DANGER ) )
+ {
+ return SCHED_FLINCH_PHYSICS;
+ }
+
+ // grunts place HIGH priority on running away from danger sounds.
+ if ( HasCondition(COND_HEAR_DANGER) )
+ {
+ CSound *pSound;
+ pSound = GetBestSound();
+
+ Assert( pSound != NULL );
+ if ( pSound)
+ {
+ if (pSound->m_iType & SOUND_DANGER)
+ {
+ // I hear something dangerous, probably need to take cover.
+ // dangerous sound nearby!, call it out
+ const char *pSentenceName = "COMBINE_DANGER";
+
+ CBaseEntity *pSoundOwner = pSound->m_hOwner;
+ if ( pSoundOwner )
+ {
+ CBaseGrenade *pGrenade = dynamic_cast<CBaseGrenade *>(pSoundOwner);
+ if ( pGrenade && pGrenade->GetThrower() )
+ {
+ if ( IRelationType( pGrenade->GetThrower() ) != D_LI )
+ {
+ // special case call out for enemy grenades
+ pSentenceName = "COMBINE_GREN";
+ }
+ }
+ }
+
+ m_Sentences.Speak( pSentenceName, SENTENCE_PRIORITY_NORMAL, SENTENCE_CRITERIA_NORMAL );
+
+ // If the sound is approaching danger, I have no enemy, and I don't see it, turn to face.
+ if( !GetEnemy() && pSound->IsSoundType(SOUND_CONTEXT_DANGER_APPROACH) && pSound->m_hOwner && !FInViewCone(pSound->GetSoundReactOrigin()) )
+ {
+ GetMotor()->SetIdealYawToTarget( pSound->GetSoundReactOrigin() );
+ return SCHED_COMBINE_FACE_IDEAL_YAW;
+ }
+
+ return SCHED_TAKE_COVER_FROM_BEST_SOUND;
+ }
+
+ // JAY: This was disabled in HL1. Test?
+ if (!HasCondition( COND_SEE_ENEMY ) && ( pSound->m_iType & (SOUND_PLAYER | SOUND_COMBAT) ))
+ {
+ GetMotor()->SetIdealYawToTarget( pSound->GetSoundReactOrigin() );
+ }
+ }
+ }
+ }
+
+ if( BehaviorSelectSchedule() )
+ {
+ return BaseClass::SelectSchedule();
+ }
+ }
+
+ switch ( m_NPCState )
+ {
+ case NPC_STATE_IDLE:
+ {
+ if ( m_bShouldPatrol )
+ return SCHED_COMBINE_PATROL;
+ }
+ // NOTE: Fall through!
+
+ case NPC_STATE_ALERT:
+ {
+ if( HasCondition(COND_LIGHT_DAMAGE) || HasCondition(COND_HEAVY_DAMAGE) )
+ {
+ AI_EnemyInfo_t *pDanger = GetEnemies()->GetDangerMemory();
+ if( pDanger && FInViewCone(pDanger->vLastKnownLocation) && !BaseClass::FVisible(pDanger->vLastKnownLocation) )
+ {
+ // I've been hurt, I'm facing the danger, but I don't see it, so move from this position.
+ return SCHED_TAKE_COVER_FROM_ORIGIN;
+ }
+ }
+
+ if( HasCondition( COND_HEAR_COMBAT ) )
+ {
+ CSound *pSound = GetBestSound();
+
+ if( pSound && pSound->IsSoundType( SOUND_COMBAT ) )
+ {
+ if( m_pSquad && m_pSquad->GetSquadMemberNearestTo( pSound->GetSoundReactOrigin() ) == this && OccupyStrategySlot( SQUAD_SLOT_INVESTIGATE_SOUND ) )
+ {
+ return SCHED_INVESTIGATE_SOUND;
+ }
+ }
+ }
+
+ // Don't patrol if I'm in the middle of an assault, because I'll never return to the assault.
+ if ( !m_AssaultBehavior.HasAssaultCue() )
+ {
+ if( m_bShouldPatrol || HasCondition( COND_COMBINE_SHOULD_PATROL ) )
+ return SCHED_COMBINE_PATROL;
+ }
+ }
+ break;
+
+ case NPC_STATE_COMBAT:
+ {
+ int nSched = SelectCombatSchedule();
+ if ( nSched != SCHED_NONE )
+ return nSched;
+ }
+ break;
+ }
+
+ // no special cases here, call the base class
+ return BaseClass::SelectSchedule();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+int CNPC_Combine::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode )
+{
+ if( failedSchedule == SCHED_COMBINE_TAKE_COVER1 )
+ {
+ if( IsInSquad() && IsStrategySlotRangeOccupied(SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2) && HasCondition(COND_SEE_ENEMY) )
+ {
+ // This eases the effects of an unfortunate bug that usually plagues shotgunners. Since their rate of fire is low,
+ // they spend relatively long periods of time without an attack squad slot. If you corner a shotgunner, usually
+ // the other memebers of the squad will hog all of the attack slots and pick schedules to move to establish line of
+ // fire. During this time, the shotgunner is prevented from attacking. If he also cannot find cover (the fallback case)
+ // he will stand around like an idiot, right in front of you. Instead of this, we have him run up to you for a melee attack.
+ return SCHED_COMBINE_MOVE_TO_MELEE;
+ }
+ }
+
+ return BaseClass::SelectFailSchedule( failedSchedule, failedTask, taskFailCode );
+}
+
+//-----------------------------------------------------------------------------
+// Should we charge the player?
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::ShouldChargePlayer()
+{
+ return GetEnemy() && GetEnemy()->IsPlayer() && PlayerHasMegaPhysCannon() && !IsLimitingHintGroups();
+}
+
+
+//-----------------------------------------------------------------------------
+// Select attack schedules
+//-----------------------------------------------------------------------------
+#define COMBINE_MEGA_PHYSCANNON_ATTACK_DISTANCE 192
+#define COMBINE_MEGA_PHYSCANNON_ATTACK_DISTANCE_SQ (COMBINE_MEGA_PHYSCANNON_ATTACK_DISTANCE*COMBINE_MEGA_PHYSCANNON_ATTACK_DISTANCE)
+
+int CNPC_Combine::SelectScheduleAttack()
+{
+ // Drop a grenade?
+ if ( HasCondition( COND_COMBINE_DROP_GRENADE ) )
+ return SCHED_COMBINE_DROP_GRENADE;
+
+ // Kick attack?
+ if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) )
+ {
+ return SCHED_MELEE_ATTACK1;
+ }
+
+ // If I'm fighting a combine turret (it's been hacked to attack me), I can't really
+ // hurt it with bullets, so become grenade happy.
+ if ( GetEnemy() && GetEnemy()->Classify() == CLASS_COMBINE && FClassnameIs(GetEnemy(), "npc_turret_floor") )
+ {
+ // Don't do this until I've been fighting the turret for a few seconds
+ float flTimeAtFirstHand = GetEnemies()->TimeAtFirstHand(GetEnemy());
+ if ( flTimeAtFirstHand != AI_INVALID_TIME )
+ {
+ float flTimeEnemySeen = gpGlobals->curtime - flTimeAtFirstHand;
+ if ( flTimeEnemySeen > 4.0 )
+ {
+ if ( CanGrenadeEnemy() && OccupyStrategySlot( SQUAD_SLOT_GRENADE1 ) )
+ return SCHED_RANGE_ATTACK2;
+ }
+ }
+
+ // If we're not in the viewcone of the turret, run up and hit it. Do this a bit later to
+ // give other squadmembers a chance to throw a grenade before I run in.
+ if ( !GetEnemy()->MyNPCPointer()->FInViewCone( this ) && OccupyStrategySlot( SQUAD_SLOT_GRENADE1 ) )
+ return SCHED_COMBINE_CHARGE_TURRET;
+ }
+
+ // When fighting against the player who's wielding a mega-physcannon,
+ // always close the distance if possible
+ // But don't do it if you're in a nav-limited hint group
+ if ( ShouldChargePlayer() )
+ {
+ float flDistSq = GetEnemy()->WorldSpaceCenter().DistToSqr( WorldSpaceCenter() );
+ if ( flDistSq <= COMBINE_MEGA_PHYSCANNON_ATTACK_DISTANCE_SQ )
+ {
+ if( HasCondition(COND_SEE_ENEMY) )
+ {
+ if ( OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ return SCHED_RANGE_ATTACK1;
+ }
+ else
+ {
+ if ( OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ return SCHED_COMBINE_PRESS_ATTACK;
+ }
+ }
+
+ if ( HasCondition(COND_SEE_ENEMY) && !IsUnreachable( GetEnemy() ) )
+ {
+ return SCHED_COMBINE_CHARGE_PLAYER;
+ }
+ }
+
+ // Can I shoot?
+ if ( HasCondition(COND_CAN_RANGE_ATTACK1) )
+ {
+
+ // JAY: HL1 behavior missing?
+#if 0
+ if ( m_pSquad )
+ {
+ // if the enemy has eluded the squad and a squad member has just located the enemy
+ // and the enemy does not see the squad member, issue a call to the squad to waste a
+ // little time and give the player a chance to turn.
+ if ( MySquadLeader()->m_fEnemyEluded && !HasConditions ( bits_COND_ENEMY_FACING_ME ) )
+ {
+ MySquadLeader()->m_fEnemyEluded = FALSE;
+ return SCHED_GRUNT_FOUND_ENEMY;
+ }
+ }
+#endif
+
+ // Engage if allowed
+ if ( OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ {
+ return SCHED_RANGE_ATTACK1;
+ }
+
+ // Throw a grenade if not allowed to engage with weapon.
+ if ( CanGrenadeEnemy() )
+ {
+ if ( OccupyStrategySlot( SQUAD_SLOT_GRENADE1 ) )
+ {
+ return SCHED_RANGE_ATTACK2;
+ }
+ }
+
+ DesireCrouch();
+ return SCHED_TAKE_COVER_FROM_ENEMY;
+ }
+
+ if ( GetEnemy() && !HasCondition(COND_SEE_ENEMY) )
+ {
+ // We don't see our enemy. If it hasn't been long since I last saw him,
+ // and he's pretty close to the last place I saw him, throw a grenade in
+ // to flush him out. A wee bit of cheating here...
+
+ float flTime;
+ float flDist;
+
+ flTime = gpGlobals->curtime - GetEnemies()->LastTimeSeen( GetEnemy() );
+ flDist = ( GetEnemy()->GetAbsOrigin() - GetEnemies()->LastSeenPosition( GetEnemy() ) ).Length();
+
+ //Msg("Time: %f Dist: %f\n", flTime, flDist );
+ if ( flTime <= COMBINE_GRENADE_FLUSH_TIME && flDist <= COMBINE_GRENADE_FLUSH_DIST && CanGrenadeEnemy( false ) && OccupyStrategySlot( SQUAD_SLOT_GRENADE1 ) )
+ {
+ return SCHED_RANGE_ATTACK2;
+ }
+ }
+
+ if (HasCondition(COND_WEAPON_SIGHT_OCCLUDED))
+ {
+ // If they are hiding behind something that we can destroy, start shooting at it.
+ CBaseEntity *pBlocker = GetEnemyOccluder();
+ if ( pBlocker && pBlocker->GetHealth() > 0 && OccupyStrategySlot( SQUAD_SLOT_ATTACK_OCCLUDER ) )
+ {
+ return SCHED_SHOOT_ENEMY_COVER;
+ }
+ }
+
+ return SCHED_NONE;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+int CNPC_Combine::TranslateSchedule( int scheduleType )
+{
+ switch( scheduleType )
+ {
+ case SCHED_TAKE_COVER_FROM_ENEMY:
+ {
+ if ( m_pSquad )
+ {
+ // Have to explicitly check innate range attack condition as may have weapon with range attack 2
+ if ( g_pGameRules->IsSkillLevel( SKILL_HARD ) &&
+ HasCondition(COND_CAN_RANGE_ATTACK2) &&
+ OccupyStrategySlot( SQUAD_SLOT_GRENADE1 ) )
+ {
+ m_Sentences.Speak( "COMBINE_THROW_GRENADE" );
+ return SCHED_COMBINE_TOSS_GRENADE_COVER1;
+ }
+ else
+ {
+ if ( ShouldChargePlayer() && !IsUnreachable( GetEnemy() ) )
+ return SCHED_COMBINE_CHARGE_PLAYER;
+
+ return SCHED_COMBINE_TAKE_COVER1;
+ }
+ }
+ else
+ {
+ // Have to explicitly check innate range attack condition as may have weapon with range attack 2
+ if ( random->RandomInt(0,1) && HasCondition(COND_CAN_RANGE_ATTACK2) )
+ {
+ return SCHED_COMBINE_GRENADE_COVER1;
+ }
+ else
+ {
+ if ( ShouldChargePlayer() && !IsUnreachable( GetEnemy() ) )
+ return SCHED_COMBINE_CHARGE_PLAYER;
+
+ return SCHED_COMBINE_TAKE_COVER1;
+ }
+ }
+ }
+ case SCHED_TAKE_COVER_FROM_BEST_SOUND:
+ {
+ return SCHED_COMBINE_TAKE_COVER_FROM_BEST_SOUND;
+ }
+ break;
+ case SCHED_COMBINE_TAKECOVER_FAILED:
+ {
+ if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) && OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ {
+ return TranslateSchedule( SCHED_RANGE_ATTACK1 );
+ }
+
+ // Run somewhere randomly
+ return TranslateSchedule( SCHED_FAIL );
+ break;
+ }
+ break;
+ case SCHED_FAIL_ESTABLISH_LINE_OF_FIRE:
+ {
+ if( !IsCrouching() )
+ {
+ if( GetEnemy() && CouldShootIfCrouching( GetEnemy() ) )
+ {
+ Crouch();
+ return SCHED_COMBAT_FACE;
+ }
+ }
+
+ if( HasCondition( COND_SEE_ENEMY ) )
+ {
+ return TranslateSchedule( SCHED_TAKE_COVER_FROM_ENEMY );
+ }
+ else if ( !m_AssaultBehavior.HasAssaultCue() )
+ {
+ // Don't patrol if I'm in the middle of an assault, because
+ // I'll never return to the assault.
+ if ( GetEnemy() )
+ {
+ RememberUnreachable( GetEnemy() );
+ }
+
+ return TranslateSchedule( SCHED_COMBINE_PATROL );
+ }
+ }
+ break;
+ case SCHED_COMBINE_ASSAULT:
+ {
+ CBaseEntity *pEntity = GetEnemy();
+
+ // FIXME: this should be generalized by the schedules that are selected, or in the definition of
+ // what "cover" means (i.e., trace attack vulnerability vs. physical attack vulnerability
+ if (pEntity && pEntity->MyNPCPointer())
+ {
+ if ( !(pEntity->MyNPCPointer()->CapabilitiesGet( ) & bits_CAP_WEAPON_RANGE_ATTACK1))
+ {
+ return TranslateSchedule( SCHED_ESTABLISH_LINE_OF_FIRE );
+ }
+ }
+ // don't charge forward if there's a hint group
+ if (GetHintGroup() != NULL_STRING)
+ {
+ return TranslateSchedule( SCHED_ESTABLISH_LINE_OF_FIRE );
+ }
+ return SCHED_COMBINE_ASSAULT;
+ }
+ case SCHED_ESTABLISH_LINE_OF_FIRE:
+ {
+ // always assume standing
+ // Stand();
+
+ if( CanAltFireEnemy(true) && OccupyStrategySlot(SQUAD_SLOT_SPECIAL_ATTACK) )
+ {
+ // If an elite in the squad could fire a combine ball at the player's last known position,
+ // do so!
+ return SCHED_COMBINE_AR2_ALTFIRE;
+ }
+
+ if( IsUsingTacticalVariant( TACTICAL_VARIANT_PRESSURE_ENEMY ) && !IsRunningBehavior() )
+ {
+ if( OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ {
+ return SCHED_COMBINE_PRESS_ATTACK;
+ }
+ }
+
+ return SCHED_COMBINE_ESTABLISH_LINE_OF_FIRE;
+ }
+ break;
+ case SCHED_HIDE_AND_RELOAD:
+ {
+ // stand up, just in case
+ // Stand();
+ // DesireStand();
+ if( CanGrenadeEnemy() && OccupyStrategySlot( SQUAD_SLOT_GRENADE1 ) && random->RandomInt( 0, 100 ) < 20 )
+ {
+ // If I COULD throw a grenade and I need to reload, 20% chance I'll throw a grenade before I hide to reload.
+ return SCHED_COMBINE_GRENADE_AND_RELOAD;
+ }
+
+ // No running away in the citadel!
+ if ( ShouldChargePlayer() )
+ return SCHED_RELOAD;
+
+ return SCHED_COMBINE_HIDE_AND_RELOAD;
+ }
+ break;
+ case SCHED_RANGE_ATTACK1:
+ {
+ if ( HasCondition( COND_NO_PRIMARY_AMMO ) || HasCondition( COND_LOW_PRIMARY_AMMO ) )
+ {
+ // Ditch the strategy slot for attacking (which we just reserved!)
+ VacateStrategySlot();
+ return TranslateSchedule( SCHED_HIDE_AND_RELOAD );
+ }
+
+ if( CanAltFireEnemy(true) && OccupyStrategySlot(SQUAD_SLOT_SPECIAL_ATTACK) )
+ {
+ // Since I'm holding this squadslot, no one else can try right now. If I die before the shot
+ // goes off, I won't have affected anyone else's ability to use this attack at their nearest
+ // convenience.
+ return SCHED_COMBINE_AR2_ALTFIRE;
+ }
+
+ if ( IsCrouching() || ( CrouchIsDesired() && !HasCondition( COND_HEAVY_DAMAGE ) ) )
+ {
+ // See if we can crouch and shoot
+ if (GetEnemy() != NULL)
+ {
+ float dist = (GetLocalOrigin() - GetEnemy()->GetLocalOrigin()).Length();
+
+ // only crouch if they are relatively far away
+ if (dist > COMBINE_MIN_CROUCH_DISTANCE)
+ {
+ // try crouching
+ Crouch();
+
+ Vector targetPos = GetEnemy()->BodyTarget(GetActiveWeapon()->GetLocalOrigin());
+
+ // if we can't see it crouched, stand up
+ if (!WeaponLOSCondition(GetLocalOrigin(),targetPos,false))
+ {
+ Stand();
+ }
+ }
+ }
+ }
+ else
+ {
+ // always assume standing
+ Stand();
+ }
+
+ return SCHED_COMBINE_RANGE_ATTACK1;
+ }
+ case SCHED_RANGE_ATTACK2:
+ {
+ // If my weapon can range attack 2 use the weapon
+ if (GetActiveWeapon() && GetActiveWeapon()->CapabilitiesGet() & bits_CAP_WEAPON_RANGE_ATTACK2)
+ {
+ return SCHED_RANGE_ATTACK2;
+ }
+ // Otherwise use innate attack
+ else
+ {
+ return SCHED_COMBINE_RANGE_ATTACK2;
+ }
+ }
+ // SCHED_COMBAT_FACE:
+ // SCHED_COMBINE_WAIT_FACE_ENEMY:
+ // SCHED_COMBINE_SWEEP:
+ // SCHED_COMBINE_COVER_AND_RELOAD:
+ // SCHED_COMBINE_FOUND_ENEMY:
+
+ case SCHED_VICTORY_DANCE:
+ {
+ return SCHED_COMBINE_VICTORY_DANCE;
+ }
+ case SCHED_COMBINE_SUPPRESS:
+ {
+#define MIN_SIGNAL_DIST 256
+ if ( GetEnemy() != NULL && GetEnemy()->IsPlayer() && m_bFirstEncounter )
+ {
+ float flDistToEnemy = ( GetEnemy()->GetAbsOrigin() - GetAbsOrigin() ).Length();
+
+ if( flDistToEnemy >= MIN_SIGNAL_DIST )
+ {
+ m_bFirstEncounter = false;// after first encounter, leader won't issue handsigns anymore when he has a new enemy
+ return SCHED_COMBINE_SIGNAL_SUPPRESS;
+ }
+ }
+
+ return SCHED_COMBINE_SUPPRESS;
+ }
+ case SCHED_FAIL:
+ {
+ if ( GetEnemy() != NULL )
+ {
+ return SCHED_COMBINE_COMBAT_FAIL;
+ }
+ return SCHED_FAIL;
+ }
+
+ case SCHED_COMBINE_PATROL:
+ {
+ // If I have an enemy, don't go off into random patrol mode.
+ if ( GetEnemy() && GetEnemy()->IsAlive() )
+ return SCHED_COMBINE_PATROL_ENEMY;
+
+ return SCHED_COMBINE_PATROL;
+ }
+ }
+
+ return BaseClass::TranslateSchedule( scheduleType );
+}
+
+//=========================================================
+//=========================================================
+void CNPC_Combine::OnStartSchedule( int scheduleType )
+{
+}
+
+//=========================================================
+// HandleAnimEvent - catches the monster-specific messages
+// that occur when tagged animation frames are played.
+//=========================================================
+void CNPC_Combine::HandleAnimEvent( animevent_t *pEvent )
+{
+ Vector vecShootDir;
+ Vector vecShootOrigin;
+ bool handledEvent = false;
+
+ if (pEvent->type & AE_TYPE_NEWEVENTSYSTEM)
+ {
+ if ( pEvent->event == COMBINE_AE_BEGIN_ALTFIRE )
+ {
+ EmitSound( "Weapon_CombineGuard.Special1" );
+ handledEvent = true;
+ }
+ else if ( pEvent->event == COMBINE_AE_ALTFIRE )
+ {
+ if( IsElite() )
+ {
+ animevent_t fakeEvent;
+
+ fakeEvent.pSource = this;
+ fakeEvent.event = EVENT_WEAPON_AR2_ALTFIRE;
+ GetActiveWeapon()->Operator_HandleAnimEvent( &fakeEvent, this );
+
+ // Stop other squad members from combine balling for a while.
+ DelaySquadAltFireAttack( 10.0f );
+
+ // I'm disabling this decrementor. At the time of this change, the elites
+ // don't bother to check if they have grenades anyway. This means that all
+ // elites have infinite combine balls, even if the designer marks the elite
+ // as having 0 grenades. By disabling this decrementor, yet enabling the code
+ // that makes sure the elite has grenades in order to fire a combine ball, we
+ // preserve the legacy behavior while making it possible for a designer to prevent
+ // elites from shooting combine balls by setting grenades to '0' in hammer. (sjb) EP2_OUTLAND_10
+ // m_iNumGrenades--;
+ }
+
+ handledEvent = true;
+ }
+ else
+ {
+ BaseClass::HandleAnimEvent( pEvent );
+ }
+ }
+ else
+ {
+ switch( pEvent->event )
+ {
+ case COMBINE_AE_AIM:
+ {
+ handledEvent = true;
+ break;
+ }
+ case COMBINE_AE_RELOAD:
+
+ // We never actually run out of ammo, just need to refill the clip
+ if (GetActiveWeapon())
+ {
+ GetActiveWeapon()->WeaponSound( RELOAD_NPC );
+ GetActiveWeapon()->m_iClip1 = GetActiveWeapon()->GetMaxClip1();
+ GetActiveWeapon()->m_iClip2 = GetActiveWeapon()->GetMaxClip2();
+ }
+ ClearCondition(COND_LOW_PRIMARY_AMMO);
+ ClearCondition(COND_NO_PRIMARY_AMMO);
+ ClearCondition(COND_NO_SECONDARY_AMMO);
+ handledEvent = true;
+ break;
+
+ case COMBINE_AE_GREN_TOSS:
+ {
+ Vector vecSpin;
+ vecSpin.x = random->RandomFloat( -1000.0, 1000.0 );
+ vecSpin.y = random->RandomFloat( -1000.0, 1000.0 );
+ vecSpin.z = random->RandomFloat( -1000.0, 1000.0 );
+
+ Vector vecStart;
+ GetAttachment( "lefthand", vecStart );
+
+ if( m_NPCState == NPC_STATE_SCRIPT )
+ {
+ // Use a fixed velocity for grenades thrown in scripted state.
+ // Grenades thrown from a script do not count against grenades remaining for the AI to use.
+ Vector forward, up, vecThrow;
+
+ GetVectors( &forward, NULL, &up );
+ vecThrow = forward * 750 + up * 175;
+ Fraggrenade_Create( vecStart, vec3_angle, vecThrow, vecSpin, this, COMBINE_GRENADE_TIMER, true );
+ }
+ else
+ {
+ // Use the Velocity that AI gave us.
+ Fraggrenade_Create( vecStart, vec3_angle, m_vecTossVelocity, vecSpin, this, COMBINE_GRENADE_TIMER, true );
+ m_iNumGrenades--;
+ }
+
+ // wait six seconds before even looking again to see if a grenade can be thrown.
+ m_flNextGrenadeCheck = gpGlobals->curtime + 6;
+ }
+ handledEvent = true;
+ break;
+
+ case COMBINE_AE_GREN_LAUNCH:
+ {
+ EmitSound( "NPC_Combine.GrenadeLaunch" );
+
+ CBaseEntity *pGrenade = CreateNoSpawn( "npc_contactgrenade", Weapon_ShootPosition(), vec3_angle, this );
+ pGrenade->KeyValue( "velocity", m_vecTossVelocity );
+ pGrenade->Spawn( );
+
+ if ( g_pGameRules->IsSkillLevel(SKILL_HARD) )
+ m_flNextGrenadeCheck = gpGlobals->curtime + random->RandomFloat( 2, 5 );// wait a random amount of time before shooting again
+ else
+ m_flNextGrenadeCheck = gpGlobals->curtime + 6;// wait six seconds before even looking again to see if a grenade can be thrown.
+ }
+ handledEvent = true;
+ break;
+
+ case COMBINE_AE_GREN_DROP:
+ {
+ Vector vecStart;
+ GetAttachment( "lefthand", vecStart );
+
+ Fraggrenade_Create( vecStart, vec3_angle, m_vecTossVelocity, vec3_origin, this, COMBINE_GRENADE_TIMER, true );
+ m_iNumGrenades--;
+ }
+ handledEvent = true;
+ break;
+
+ case COMBINE_AE_KICK:
+ {
+ // Does no damage, because damage is applied based upon whether the target can handle the interaction
+ CBaseEntity *pHurt = CheckTraceHullAttack( 70, -Vector(16,16,18), Vector(16,16,18), 0, DMG_CLUB );
+ CBaseCombatCharacter* pBCC = ToBaseCombatCharacter( pHurt );
+ if (pBCC)
+ {
+ Vector forward, up;
+ AngleVectors( GetLocalAngles(), &forward, NULL, &up );
+
+ if ( !pBCC->DispatchInteraction( g_interactionCombineBash, NULL, this ) )
+ {
+ if ( pBCC->IsPlayer() )
+ {
+ pBCC->ViewPunch( QAngle(-12,-7,0) );
+ pHurt->ApplyAbsVelocityImpulse( forward * 100 + up * 50 );
+ }
+
+ CTakeDamageInfo info( this, this, m_nKickDamage, DMG_CLUB );
+ CalculateMeleeDamageForce( &info, forward, pBCC->GetAbsOrigin() );
+ pBCC->TakeDamage( info );
+
+ EmitSound( "NPC_Combine.WeaponBash" );
+ }
+ }
+
+ m_Sentences.Speak( "COMBINE_KICK" );
+ handledEvent = true;
+ break;
+ }
+
+ case COMBINE_AE_CAUGHT_ENEMY:
+ m_Sentences.Speak( "COMBINE_ALERT" );
+ handledEvent = true;
+ break;
+
+ default:
+ BaseClass::HandleAnimEvent( pEvent );
+ break;
+ }
+ }
+
+ if( handledEvent )
+ {
+ m_iLastAnimEventHandled = pEvent->event;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Get shoot position of BCC at an arbitrary position
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+Vector CNPC_Combine::Weapon_ShootPosition( )
+{
+ bool bStanding = !IsCrouching();
+ Vector right;
+ GetVectors( NULL, &right, NULL );
+
+ if ((CapabilitiesGet() & bits_CAP_DUCK) )
+ {
+ if ( IsCrouchedActivity( GetActivity() ) )
+ {
+ bStanding = false;
+ }
+ }
+
+ // FIXME: rename this "estimated" since it's not based on animation
+ // FIXME: the orientation won't be correct when testing from arbitary positions for arbitary angles
+
+ if ( bStanding )
+ {
+ if( HasShotgun() )
+ {
+ return GetAbsOrigin() + COMBINE_SHOTGUN_STANDING_POSITION + right * 8;
+ }
+ else
+ {
+ return GetAbsOrigin() + COMBINE_GUN_STANDING_POSITION + right * 8;
+ }
+ }
+ else
+ {
+ if( HasShotgun() )
+ {
+ return GetAbsOrigin() + COMBINE_SHOTGUN_CROUCHING_POSITION + right * 8;
+ }
+ else
+ {
+ return GetAbsOrigin() + COMBINE_GUN_CROUCHING_POSITION + right * 8;
+ }
+ }
+}
+
+
+//=========================================================
+// Speak Sentence - say your cued up sentence.
+//
+// Some grunt sentences (take cover and charge) rely on actually
+// being able to execute the intended action. It's really lame
+// when a grunt says 'COVER ME' and then doesn't move. The problem
+// is that the sentences were played when the decision to TRY
+// to move to cover was made. Now the sentence is played after
+// we know for sure that there is a valid path. The schedule
+// may still fail but in most cases, well after the grunt has
+// started moving.
+//=========================================================
+void CNPC_Combine::SpeakSentence( int sentenceType )
+{
+ switch( sentenceType )
+ {
+ case 0: // assault
+ AnnounceAssault();
+ break;
+
+ case 1: // Flanking the player
+ // If I'm moving more than 20ft, I need to talk about it
+ if ( GetNavigator()->GetPath()->GetPathLength() > 20 * 12.0f )
+ {
+ m_Sentences.Speak( "COMBINE_FLANK" );
+ }
+ break;
+ }
+}
+
+//=========================================================
+// PainSound
+//=========================================================
+void CNPC_Combine::PainSound ( void )
+{
+ // NOTE: The response system deals with this at the moment
+ if ( GetFlags() & FL_DISSOLVING )
+ return;
+
+ if ( gpGlobals->curtime > m_flNextPainSoundTime )
+ {
+ const char *pSentenceName = "COMBINE_PAIN";
+ float healthRatio = (float)GetHealth() / (float)GetMaxHealth();
+ if ( !HasMemory(bits_MEMORY_PAIN_LIGHT_SOUND) && healthRatio > 0.9 )
+ {
+ Remember( bits_MEMORY_PAIN_LIGHT_SOUND );
+ pSentenceName = "COMBINE_TAUNT";
+ }
+ else if ( !HasMemory(bits_MEMORY_PAIN_HEAVY_SOUND) && healthRatio < 0.25 )
+ {
+ Remember( bits_MEMORY_PAIN_HEAVY_SOUND );
+ pSentenceName = "COMBINE_COVER";
+ }
+
+ m_Sentences.Speak( pSentenceName, SENTENCE_PRIORITY_INVALID, SENTENCE_CRITERIA_ALWAYS );
+ m_flNextPainSoundTime = gpGlobals->curtime + 1;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: implemented by subclasses to give them an opportunity to make
+// a sound when they lose their enemy
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+void CNPC_Combine::LostEnemySound( void)
+{
+ if ( gpGlobals->curtime <= m_flNextLostSoundTime )
+ return;
+
+ const char *pSentence;
+ if (!(CBaseEntity*)GetEnemy() || gpGlobals->curtime - GetEnemyLastTimeSeen() > 10)
+ {
+ pSentence = "COMBINE_LOST_LONG";
+ }
+ else
+ {
+ pSentence = "COMBINE_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_Combine::FoundEnemySound( void)
+{
+ m_Sentences.Speak( "COMBINE_REFIND_ENEMY", SENTENCE_PRIORITY_HIGH );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Implemented by subclasses to give them an opportunity to make
+// a sound before they attack
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+
+// BUGBUG: It looks like this is never played because combine don't do SCHED_WAKE_ANGRY or anything else that does a TASK_SOUND_WAKE
+void CNPC_Combine::AlertSound( void)
+{
+ if ( gpGlobals->curtime > m_flNextAlertSoundTime )
+ {
+ m_Sentences.Speak( "COMBINE_GO_ALERT", SENTENCE_PRIORITY_HIGH );
+ m_flNextAlertSoundTime = gpGlobals->curtime + 10.0f;
+ }
+}
+
+//=========================================================
+// NotifyDeadFriend
+//=========================================================
+void CNPC_Combine::NotifyDeadFriend ( CBaseEntity* pFriend )
+{
+ if ( GetSquad()->NumMembers() < 2 )
+ {
+ m_Sentences.Speak( "COMBINE_LAST_OF_SQUAD", SENTENCE_PRIORITY_INVALID, SENTENCE_CRITERIA_NORMAL );
+ JustMadeSound();
+ return;
+ }
+ // relaxed visibility test so that guys say this more often
+ //if( FInViewCone( pFriend ) && FVisible( pFriend ) )
+ {
+ m_Sentences.Speak( "COMBINE_MAN_DOWN" );
+ }
+ BaseClass::NotifyDeadFriend(pFriend);
+}
+
+//=========================================================
+// DeathSound
+//=========================================================
+void CNPC_Combine::DeathSound ( void )
+{
+ // NOTE: The response system deals with this at the moment
+ if ( GetFlags() & FL_DISSOLVING )
+ return;
+
+ m_Sentences.Speak( "COMBINE_DIE", SENTENCE_PRIORITY_INVALID, SENTENCE_CRITERIA_ALWAYS );
+}
+
+//=========================================================
+// IdleSound
+//=========================================================
+void CNPC_Combine::IdleSound( void )
+{
+ if (g_fCombineQuestion || random->RandomInt(0,1))
+ {
+ if (!g_fCombineQuestion)
+ {
+ // ask question or make statement
+ switch (random->RandomInt(0,2))
+ {
+ case 0: // check in
+ if ( m_Sentences.Speak( "COMBINE_CHECK" ) >= 0 )
+ {
+ g_fCombineQuestion = 1;
+ }
+ break;
+
+ case 1: // question
+ if ( m_Sentences.Speak( "COMBINE_QUEST" ) >= 0 )
+ {
+ g_fCombineQuestion = 2;
+ }
+ break;
+
+ case 2: // statement
+ m_Sentences.Speak( "COMBINE_IDLE" );
+ break;
+ }
+ }
+ else
+ {
+ switch (g_fCombineQuestion)
+ {
+ case 1: // check in
+ if ( m_Sentences.Speak( "COMBINE_CLEAR" ) >= 0 )
+ {
+ g_fCombineQuestion = 0;
+ }
+ break;
+ case 2: // question
+ if ( m_Sentences.Speak( "COMBINE_ANSWER" ) >= 0 )
+ {
+ g_fCombineQuestion = 0;
+ }
+ break;
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//
+// This is for Grenade attacks. As the test for grenade attacks
+// is expensive we don't want to do it every frame. Return true
+// if we meet minimum set of requirements and then test for actual
+// throw later if we actually decide to do a grenade attack.
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+int CNPC_Combine::RangeAttack2Conditions( float flDot, float flDist )
+{
+ return COND_NONE;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Return true if the combine has grenades, hasn't checked lately, and
+// can throw a grenade at the target point.
+// Input : &vecTarget -
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::CanThrowGrenade( const Vector &vecTarget )
+{
+ if( m_iNumGrenades < 1 )
+ {
+ // Out of grenades!
+ return false;
+ }
+
+ if (gpGlobals->curtime < m_flNextGrenadeCheck )
+ {
+ // Not allowed to throw another grenade right now.
+ return false;
+ }
+
+ float flDist;
+ flDist = ( vecTarget - GetAbsOrigin() ).Length();
+
+ if( flDist > 1024 || flDist < 128 )
+ {
+ // Too close or too far!
+ m_flNextGrenadeCheck = gpGlobals->curtime + 1; // one full second.
+ return false;
+ }
+
+ // -----------------------
+ // If moving, don't check.
+ // -----------------------
+ if ( m_flGroundSpeed != 0 )
+ return false;
+
+#if 0
+ Vector vecEnemyLKP = GetEnemyLKP();
+ if ( !( GetEnemy()->GetFlags() & FL_ONGROUND ) && GetEnemy()->GetWaterLevel() == 0 && vecEnemyLKP.z > (GetAbsOrigin().z + WorldAlignMaxs().z) )
+ {
+ //!!!BUGBUG - we should make this check movetype and make sure it isn't FLY? Players who jump a lot are unlikely to
+ // be grenaded.
+ // don't throw grenades at anything that isn't on the ground!
+ return COND_NONE;
+ }
+#endif
+
+ // ---------------------------------------------------------------------
+ // Are any of my squad members near the intended grenade impact area?
+ // ---------------------------------------------------------------------
+ if ( m_pSquad )
+ {
+ if (m_pSquad->SquadMemberInRange( vecTarget, COMBINE_MIN_GRENADE_CLEAR_DIST ))
+ {
+ // crap, I might blow my own guy up. Don't throw a grenade and don't check again for a while.
+ m_flNextGrenadeCheck = gpGlobals->curtime + 1; // one full second.
+
+ // Tell my squad members to clear out so I can get a grenade in
+ CSoundEnt::InsertSound( SOUND_MOVE_AWAY | SOUND_CONTEXT_COMBINE_ONLY, vecTarget, COMBINE_MIN_GRENADE_CLEAR_DIST, 0.1 );
+ return false;
+ }
+ }
+
+ return CheckCanThrowGrenade( vecTarget );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Returns true if the combine can throw a grenade at the specified target point
+// Input : &vecTarget -
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::CheckCanThrowGrenade( const Vector &vecTarget )
+{
+ //NDebugOverlay::Line( EyePosition(), vecTarget, 0, 255, 0, false, 5 );
+
+ // ---------------------------------------------------------------------
+ // Check that throw is legal and clear
+ // ---------------------------------------------------------------------
+ // FIXME: this is only valid for hand grenades, not RPG's
+ Vector vecToss;
+ Vector vecMins = -Vector(4,4,4);
+ Vector vecMaxs = Vector(4,4,4);
+ if( FInViewCone( vecTarget ) && CBaseEntity::FVisible( vecTarget ) )
+ {
+ vecToss = VecCheckThrow( this, EyePosition(), vecTarget, COMBINE_GRENADE_THROW_SPEED, 1.0, &vecMins, &vecMaxs );
+ }
+ else
+ {
+ // Have to try a high toss. Do I have enough room?
+ trace_t tr;
+ AI_TraceLine( EyePosition(), EyePosition() + Vector( 0, 0, 64 ), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr );
+ if( tr.fraction != 1.0 )
+ {
+ return false;
+ }
+
+ vecToss = VecCheckToss( this, EyePosition(), vecTarget, -1, 1.0, true, &vecMins, &vecMaxs );
+ }
+
+ if ( vecToss != vec3_origin )
+ {
+ m_vecTossVelocity = vecToss;
+
+ // don't check again for a while.
+ m_flNextGrenadeCheck = gpGlobals->curtime + 1; // 1/3 second.
+ return true;
+ }
+ else
+ {
+ // don't check again for a while.
+ m_flNextGrenadeCheck = gpGlobals->curtime + 1; // one full second.
+ return false;
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::CanAltFireEnemy( bool bUseFreeKnowledge )
+{
+ if (!IsElite() )
+ return false;
+
+ if (IsCrouching())
+ return false;
+
+ if( gpGlobals->curtime < m_flNextAltFireTime )
+ return false;
+
+ if( !GetEnemy() )
+ return false;
+
+ if (gpGlobals->curtime < m_flNextGrenadeCheck )
+ return false;
+
+ // See Steve Bond if you plan on changing this next piece of code!! (SJB) EP2_OUTLAND_10
+ if (m_iNumGrenades < 1)
+ return false;
+
+ CBaseEntity *pEnemy = GetEnemy();
+
+ if( !pEnemy->IsPlayer() && (!pEnemy->IsNPC() || !pEnemy->MyNPCPointer()->IsPlayerAlly()) )
+ return false;
+
+ Vector vecTarget;
+
+ // Determine what point we're shooting at
+ if( bUseFreeKnowledge )
+ {
+ vecTarget = GetEnemies()->LastKnownPosition( pEnemy ) + (pEnemy->GetViewOffset()*0.75);// approximates the chest
+ }
+ else
+ {
+ vecTarget = GetEnemies()->LastSeenPosition( pEnemy ) + (pEnemy->GetViewOffset()*0.75);// approximates the chest
+ }
+
+ // Trace a hull about the size of the combine ball (don't shoot through grates!)
+ trace_t tr;
+
+ Vector mins( -12, -12, -12 );
+ Vector maxs( 12, 12, 12 );
+
+ Vector vShootPosition = EyePosition();
+
+ if ( GetActiveWeapon() )
+ {
+ GetActiveWeapon()->GetAttachment( "muzzle", vShootPosition );
+ }
+
+ // Trace a hull about the size of the combine ball.
+ UTIL_TraceHull( vShootPosition, vecTarget, mins, maxs, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr );
+
+ float flLength = (vShootPosition - vecTarget).Length();
+
+ flLength *= tr.fraction;
+
+ //If the ball can travel at least 65% of the distance to the player then let the NPC shoot it.
+ if( tr.fraction >= 0.65 && flLength > 128.0f )
+ {
+ // Target is valid
+ m_vecAltFireTarget = vecTarget;
+ return true;
+ }
+
+
+ // Check again later
+ m_vecAltFireTarget = vec3_origin;
+ m_flNextGrenadeCheck = gpGlobals->curtime + 1.0f;
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::CanGrenadeEnemy( bool bUseFreeKnowledge )
+{
+ if( IsElite() )
+ return false;
+
+ CBaseEntity *pEnemy = GetEnemy();
+
+ Assert( pEnemy != NULL );
+
+ if( pEnemy )
+ {
+ // I'm not allowed to throw grenades during dustoff
+ if ( IsCurSchedule(SCHED_DROPSHIP_DUSTOFF) )
+ return false;
+
+ if( bUseFreeKnowledge )
+ {
+ // throw to where we think they are.
+ return CanThrowGrenade( GetEnemies()->LastKnownPosition( pEnemy ) );
+ }
+ else
+ {
+ // hafta throw to where we last saw them.
+ return CanThrowGrenade( GetEnemies()->LastSeenPosition( pEnemy ) );
+ }
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: For combine melee attack (kick/hit)
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+int CNPC_Combine::MeleeAttack1Conditions ( float flDot, float flDist )
+{
+ if (flDist > 64)
+ {
+ return COND_NONE; // COND_TOO_FAR_TO_ATTACK;
+ }
+ else if (flDot < 0.7)
+ {
+ return COND_NONE; // COND_NOT_FACING_ATTACK;
+ }
+
+ // Check Z
+ if ( GetEnemy() && fabs(GetEnemy()->GetAbsOrigin().z - GetAbsOrigin().z) > 64 )
+ return COND_NONE;
+
+ if ( dynamic_cast<CBaseHeadcrab *>(GetEnemy()) != NULL )
+ {
+ return COND_NONE;
+ }
+
+ // Make sure not trying to kick through a window or something.
+ trace_t tr;
+ Vector vecSrc, vecEnd;
+
+ vecSrc = WorldSpaceCenter();
+ vecEnd = GetEnemy()->WorldSpaceCenter();
+
+ AI_TraceLine(vecSrc, vecEnd, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr);
+ if( tr.m_pEnt != GetEnemy() )
+ {
+ return COND_NONE;
+ }
+
+ return COND_CAN_MELEE_ATTACK1;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Output : Vector
+//-----------------------------------------------------------------------------
+Vector CNPC_Combine::EyePosition( void )
+{
+ if ( !IsCrouching() )
+ {
+ return GetAbsOrigin() + COMBINE_EYE_STANDING_POSITION;
+ }
+ else
+ {
+ return GetAbsOrigin() + COMBINE_EYE_CROUCHING_POSITION;
+ }
+
+ /*
+ Vector m_EyePos;
+ GetAttachment( "eyes", m_EyePos );
+ return m_EyePos;
+ */
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+Vector CNPC_Combine::GetAltFireTarget()
+{
+ Assert( IsElite() );
+
+ return m_vecAltFireTarget;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : nActivity -
+// Output : Vector
+//-----------------------------------------------------------------------------
+Vector CNPC_Combine::EyeOffset( Activity nActivity )
+{
+ if (CapabilitiesGet() & bits_CAP_DUCK)
+ {
+ if ( IsCrouchedActivity( nActivity ) )
+ return COMBINE_EYE_CROUCHING_POSITION;
+
+ }
+ // if the hint doesn't tell anything, assume current state
+ if ( !IsCrouching() )
+ {
+ return COMBINE_EYE_STANDING_POSITION;
+ }
+ else
+ {
+ return COMBINE_EYE_CROUCHING_POSITION;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+Vector CNPC_Combine::GetCrouchEyeOffset( void )
+{
+ return COMBINE_EYE_CROUCHING_POSITION;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_Combine::SetActivity( Activity NewActivity )
+{
+ BaseClass::SetActivity( NewActivity );
+
+ m_iLastAnimEventHandled = -1;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+NPC_STATE CNPC_Combine::SelectIdealState( void )
+{
+ switch ( m_NPCState )
+ {
+ case NPC_STATE_COMBAT:
+ {
+ if ( GetEnemy() == NULL )
+ {
+ if ( !HasCondition( COND_ENEMY_DEAD ) )
+ {
+ // Lost track of my enemy. Patrol.
+ SetCondition( COND_COMBINE_SHOULD_PATROL );
+ }
+ return NPC_STATE_ALERT;
+ }
+ else if ( HasCondition( COND_ENEMY_DEAD ) )
+ {
+ AnnounceEnemyKill(GetEnemy());
+ }
+ }
+
+ default:
+ {
+ return BaseClass::SelectIdealState();
+ }
+ }
+
+ return GetIdealState();
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::OnBeginMoveAndShoot()
+{
+ if ( BaseClass::OnBeginMoveAndShoot() )
+ {
+ if( HasStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ return true; // already have the slot I need
+
+ if( !HasStrategySlotRange( SQUAD_SLOT_GRENADE1, SQUAD_SLOT_ATTACK_OCCLUDER ) && OccupyStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ return true;
+ }
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_Combine::OnEndMoveAndShoot()
+{
+ VacateStrategySlot();
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+WeaponProficiency_t CNPC_Combine::CalcWeaponProficiency( CBaseCombatWeapon *pWeapon )
+{
+ if( FClassnameIs( pWeapon, "weapon_ar2" ) )
+ {
+ if( hl2_episodic.GetBool() )
+ {
+ return WEAPON_PROFICIENCY_VERY_GOOD;
+ }
+ else
+ {
+ return WEAPON_PROFICIENCY_GOOD;
+ }
+ }
+ else if( FClassnameIs( pWeapon, "weapon_shotgun" ) )
+ {
+ if( m_nSkin != COMBINE_SKIN_SHOTGUNNER )
+ {
+ m_nSkin = COMBINE_SKIN_SHOTGUNNER;
+ }
+
+ return WEAPON_PROFICIENCY_PERFECT;
+ }
+ else if( FClassnameIs( pWeapon, "weapon_smg1" ) )
+ {
+ return WEAPON_PROFICIENCY_GOOD;
+ }
+
+ return BaseClass::CalcWeaponProficiency( pWeapon );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::HasShotgun()
+{
+ if( GetActiveWeapon() && GetActiveWeapon()->m_iClassname == s_iszShotgunClassname )
+ {
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Only supports weapons that use clips.
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::ActiveWeaponIsFullyLoaded()
+{
+ CBaseCombatWeapon *pWeapon = GetActiveWeapon();
+
+ if( !pWeapon )
+ return false;
+
+ if( !pWeapon->UsesClipsForAmmo1() )
+ return false;
+
+ return ( pWeapon->Clip1() >= pWeapon->GetMaxClip1() );
+}
+
+
+//-----------------------------------------------------------------------------
+// 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 : The type of interaction, extra info pointer, and who started it
+// Output : true - if sub-class has a response for the interaction
+// false - if sub-class has no response
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter *sourceEnt)
+{
+ if ( interactionType == g_interactionTurretStillStanding )
+ {
+ // A turret that I've kicked recently is still standing 5 seconds later.
+ if ( sourceEnt == GetEnemy() )
+ {
+ // It's still my enemy. Time to grenade it.
+ Vector forward, up;
+ AngleVectors( GetLocalAngles(), &forward, NULL, &up );
+ m_vecTossVelocity = forward * 10;
+ SetCondition( COND_COMBINE_DROP_GRENADE );
+ ClearSchedule( "Failed to kick over turret" );
+ }
+ return true;
+ }
+
+ return BaseClass::HandleInteraction( interactionType, data, sourceEnt );
+}
+
+//-----------------------------------------------------------------------------
+//
+//-----------------------------------------------------------------------------
+const char* CNPC_Combine::GetSquadSlotDebugName( int iSquadSlot )
+{
+ switch( iSquadSlot )
+ {
+ case SQUAD_SLOT_GRENADE1: return "SQUAD_SLOT_GRENADE1";
+ break;
+ case SQUAD_SLOT_GRENADE2: return "SQUAD_SLOT_GRENADE2";
+ break;
+ case SQUAD_SLOT_ATTACK_OCCLUDER: return "SQUAD_SLOT_ATTACK_OCCLUDER";
+ break;
+ case SQUAD_SLOT_OVERWATCH: return "SQUAD_SLOT_OVERWATCH";
+ break;
+ }
+
+ return BaseClass::GetSquadSlotDebugName( iSquadSlot );
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::IsUsingTacticalVariant( int variant )
+{
+ if( variant == TACTICAL_VARIANT_PRESSURE_ENEMY && m_iTacticalVariant == TACTICAL_VARIANT_PRESSURE_ENEMY_UNTIL_CLOSE )
+ {
+ // Essentially, fib. Just say that we are a 'pressure enemy' soldier.
+ return true;
+ }
+
+ return m_iTacticalVariant == variant;
+}
+
+//-----------------------------------------------------------------------------
+// For the purpose of determining whether to use a pathfinding variant, this
+// function determines whether the current schedule is a schedule that
+// 'approaches' the enemy.
+//-----------------------------------------------------------------------------
+bool CNPC_Combine::IsRunningApproachEnemySchedule()
+{
+ if( IsCurSchedule( SCHED_CHASE_ENEMY ) )
+ return true;
+
+ if( IsCurSchedule( SCHED_ESTABLISH_LINE_OF_FIRE ) )
+ return true;
+
+ if( IsCurSchedule( SCHED_COMBINE_PRESS_ATTACK, false ) )
+ return true;
+
+ return false;
+}
+
+bool CNPC_Combine::ShouldPickADeathPose( void )
+{
+ return !IsCrouching();
+}
+
+//-----------------------------------------------------------------------------
+//
+// Schedules
+//
+//-----------------------------------------------------------------------------
+
+AI_BEGIN_CUSTOM_NPC( npc_combine, CNPC_Combine )
+
+//Tasks
+DECLARE_TASK( TASK_COMBINE_FACE_TOSS_DIR )
+DECLARE_TASK( TASK_COMBINE_IGNORE_ATTACKS )
+DECLARE_TASK( TASK_COMBINE_SIGNAL_BEST_SOUND )
+DECLARE_TASK( TASK_COMBINE_DEFER_SQUAD_GRENADES )
+DECLARE_TASK( TASK_COMBINE_CHASE_ENEMY_CONTINUOUSLY )
+DECLARE_TASK( TASK_COMBINE_DIE_INSTANTLY )
+DECLARE_TASK( TASK_COMBINE_PLAY_SEQUENCE_FACE_ALTFIRE_TARGET )
+DECLARE_TASK( TASK_COMBINE_GET_PATH_TO_FORCED_GREN_LOS )
+DECLARE_TASK( TASK_COMBINE_SET_STANDING )
+
+//Activities
+DECLARE_ACTIVITY( ACT_COMBINE_THROW_GRENADE )
+DECLARE_ACTIVITY( ACT_COMBINE_LAUNCH_GRENADE )
+DECLARE_ACTIVITY( ACT_COMBINE_BUGBAIT )
+DECLARE_ACTIVITY( ACT_COMBINE_AR2_ALTFIRE )
+DECLARE_ACTIVITY( ACT_WALK_EASY )
+DECLARE_ACTIVITY( ACT_WALK_MARCH )
+
+DECLARE_ANIMEVENT( COMBINE_AE_BEGIN_ALTFIRE )
+DECLARE_ANIMEVENT( COMBINE_AE_ALTFIRE )
+
+DECLARE_SQUADSLOT( SQUAD_SLOT_GRENADE1 )
+DECLARE_SQUADSLOT( SQUAD_SLOT_GRENADE2 )
+
+DECLARE_CONDITION( COND_COMBINE_NO_FIRE )
+DECLARE_CONDITION( COND_COMBINE_DEAD_FRIEND )
+DECLARE_CONDITION( COND_COMBINE_SHOULD_PATROL )
+DECLARE_CONDITION( COND_COMBINE_HIT_BY_BUGBAIT )
+DECLARE_CONDITION( COND_COMBINE_DROP_GRENADE )
+DECLARE_CONDITION( COND_COMBINE_ON_FIRE )
+DECLARE_CONDITION( COND_COMBINE_ATTACK_SLOT_AVAILABLE )
+
+DECLARE_INTERACTION( g_interactionCombineBash );
+
+//=========================================================
+// SCHED_COMBINE_TAKE_COVER_FROM_BEST_SOUND
+//
+// hide from the loudest sound source (to run from grenade)
+//=========================================================
+DEFINE_SCHEDULE
+(
+ SCHED_COMBINE_TAKE_COVER_FROM_BEST_SOUND,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_COMBINE_RUN_AWAY_FROM_BEST_SOUND"
+ " TASK_STOP_MOVING 0"
+ " TASK_COMBINE_SIGNAL_BEST_SOUND 0"
+ " TASK_FIND_COVER_FROM_BEST_SOUND 0"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_REMEMBER MEMORY:INCOVER"
+ " TASK_FACE_REASONABLE 0"
+ ""
+ " Interrupts"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_RUN_AWAY_FROM_BEST_SOUND,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_COWER"
+ " TASK_GET_PATH_AWAY_FROM_BEST_SOUND 600"
+ " TASK_RUN_PATH_TIMED 2"
+ " TASK_STOP_MOVING 0"
+ ""
+ " Interrupts"
+ )
+ //=========================================================
+ // SCHED_COMBINE_COMBAT_FAIL
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_COMBAT_FAIL,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE "
+ " TASK_WAIT_FACE_ENEMY 2"
+ " TASK_WAIT_PVS 0"
+ ""
+ " Interrupts"
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_CAN_RANGE_ATTACK2"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK2"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_VICTORY_DANCE
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_VICTORY_DANCE,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_WAIT 1.5"
+ " TASK_GET_PATH_TO_ENEMY_CORPSE 0"
+ " TASK_WALK_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_PLAY_SEQUENCE ACTIVITY:ACT_VICTORY_DANCE"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_ASSAULT
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_ASSAULT,
+
+ " Tasks "
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_COMBINE_ESTABLISH_LINE_OF_FIRE"
+ " TASK_SET_TOLERANCE_DISTANCE 48"
+ " TASK_GET_PATH_TO_ENEMY_LKP 0"
+ " TASK_COMBINE_IGNORE_ATTACKS 0.2"
+ " TASK_SPEAK_SENTENCE 0"
+ " TASK_RUN_PATH 0"
+ // " TASK_COMBINE_MOVE_AND_AIM 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_COMBINE_IGNORE_ATTACKS 0.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_FAR_TO_ATTACK"
+ " COND_HEAR_DANGER"
+ " COND_HEAR_MOVE_AWAY"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_ESTABLISH_LINE_OF_FIRE,
+
+ " Tasks "
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_FAIL_ESTABLISH_LINE_OF_FIRE"
+ " TASK_SET_TOLERANCE_DISTANCE 48"
+ " TASK_GET_PATH_TO_ENEMY_LKP_LOS 0"
+ " TASK_COMBINE_SET_STANDING 1"
+ " TASK_SPEAK_SENTENCE 1"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_COMBINE_IGNORE_ATTACKS 0.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_HEAR_MOVE_AWAY"
+ " COND_HEAVY_DAMAGE"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_PRESS_ATTACK
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_PRESS_ATTACK,
+
+ " Tasks "
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_COMBINE_ESTABLISH_LINE_OF_FIRE"
+ " TASK_SET_TOLERANCE_DISTANCE 72"
+ " TASK_GET_PATH_TO_ENEMY_LKP 0"
+ " TASK_COMBINE_SET_STANDING 1"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ ""
+ " Interrupts "
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_ENEMY_UNREACHABLE"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_LOW_PRIMARY_AMMO"
+ " COND_TOO_CLOSE_TO_ATTACK"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK2"
+ " COND_HEAR_DANGER"
+ " COND_HEAR_MOVE_AWAY"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_COMBAT_FACE
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_COMBAT_FACE,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
+ " TASK_FACE_ENEMY 0"
+ " TASK_WAIT 1.5"
+ //" TASK_SET_SCHEDULE SCHEDULE:SCHED_COMBINE_SWEEP"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_CAN_RANGE_ATTACK2"
+ )
+
+ //=========================================================
+ // SCHED_HIDE_AND_RELOAD
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_HIDE_AND_RELOAD,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_RELOAD"
+ " TASK_FIND_COVER_FROM_ENEMY 0"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_REMEMBER MEMORY:INCOVER"
+ " TASK_FACE_ENEMY 0"
+ " TASK_RELOAD 0"
+ ""
+ " Interrupts"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK2"
+ " COND_HEAVY_DAMAGE"
+ " COND_HEAR_DANGER"
+ " COND_HEAR_MOVE_AWAY"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_SIGNAL_SUPPRESS
+ // don't stop shooting until the clip is
+ // empty or combine gets hurt.
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_SIGNAL_SUPPRESS,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_IDEAL 0"
+ " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_SIGNAL_GROUP"
+ " TASK_COMBINE_SET_STANDING 0"
+ " TASK_RANGE_ATTACK1 0"
+ ""
+ " Interrupts"
+ " COND_ENEMY_DEAD"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_WEAPON_BLOCKED_BY_FRIEND"
+ " COND_WEAPON_SIGHT_OCCLUDED"
+ " COND_HEAR_DANGER"
+ " COND_HEAR_MOVE_AWAY"
+ " COND_COMBINE_NO_FIRE"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_SUPPRESS
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_SUPPRESS,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_COMBINE_SET_STANDING 0"
+ " TASK_RANGE_ATTACK1 0"
+ ""
+ " Interrupts"
+ " COND_ENEMY_DEAD"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_HEAR_DANGER"
+ " COND_HEAR_MOVE_AWAY"
+ " COND_COMBINE_NO_FIRE"
+ " COND_WEAPON_BLOCKED_BY_FRIEND"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_ENTER_OVERWATCH
+ //
+ // Parks a combine soldier in place looking at the player's
+ // last known position, ready to attack if the player pops out
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_ENTER_OVERWATCH,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_COMBINE_SET_STANDING 0"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
+ " TASK_FACE_ENEMY 0"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_COMBINE_OVERWATCH"
+ ""
+ " Interrupts"
+ " COND_HEAR_DANGER"
+ " COND_NEW_ENEMY"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_OVERWATCH
+ //
+ // Parks a combine soldier in place looking at the player's
+ // last known position, ready to attack if the player pops out
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_OVERWATCH,
+
+ " Tasks"
+ " TASK_WAIT_FACE_ENEMY 10"
+ ""
+ " Interrupts"
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_ENEMY_DEAD"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_HEAR_DANGER"
+ " COND_HEAR_MOVE_AWAY"
+ " COND_NEW_ENEMY"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_WAIT_IN_COVER
+ // we don't allow danger or the ability
+ // to attack to break a combine's run to cover schedule but
+ // when a combine is in cover we do want them to attack if they can.
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_WAIT_IN_COVER,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_COMBINE_SET_STANDING 0"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" // Translated to cover
+ " TASK_WAIT_FACE_ENEMY 1"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_CAN_RANGE_ATTACK2"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK2"
+ " COND_HEAR_DANGER"
+ " COND_HEAR_MOVE_AWAY"
+ " COND_COMBINE_ATTACK_SLOT_AVAILABLE"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_TAKE_COVER1
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_TAKE_COVER1 ,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_COMBINE_TAKECOVER_FAILED"
+ " TASK_STOP_MOVING 0"
+ " TASK_WAIT 0.2"
+ " TASK_FIND_COVER_FROM_ENEMY 0"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_REMEMBER MEMORY:INCOVER"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_COMBINE_WAIT_IN_COVER"
+ ""
+ " Interrupts"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_TAKECOVER_FAILED,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ ""
+ " Interrupts"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_GRENADE_COVER1
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_GRENADE_COVER1,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FIND_COVER_FROM_ENEMY 99"
+ " TASK_FIND_FAR_NODE_COVER_FROM_ENEMY 384"
+ " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SPECIAL_ATTACK2"
+ " TASK_CLEAR_MOVE_WAIT 0"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_COMBINE_WAIT_IN_COVER"
+ ""
+ " Interrupts"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_TOSS_GRENADE_COVER1
+ //
+ // drop grenade then run to cover.
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_TOSS_GRENADE_COVER1,
+
+ " Tasks"
+ " TASK_FACE_ENEMY 0"
+ " TASK_RANGE_ATTACK2 0"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_TAKE_COVER_FROM_ENEMY"
+ ""
+ " Interrupts"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_RANGE_ATTACK1
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_RANGE_ATTACK1,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
+ " TASK_WAIT_RANDOM 0.3"
+ " TASK_RANGE_ATTACK1 0"
+ " TASK_COMBINE_IGNORE_ATTACKS 0.5"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_HEAVY_DAMAGE"
+ " COND_LIGHT_DAMAGE"
+ " COND_LOW_PRIMARY_AMMO"
+ " COND_NO_PRIMARY_AMMO"
+ " COND_WEAPON_BLOCKED_BY_FRIEND"
+ " COND_TOO_CLOSE_TO_ATTACK"
+ " COND_GIVE_WAY"
+ " COND_HEAR_DANGER"
+ " COND_HEAR_MOVE_AWAY"
+ " COND_COMBINE_NO_FIRE"
+ ""
+ // Enemy_Occluded Don't interrupt on this. Means
+ // comibine will fire where player was after
+ // he has moved for a little while. Good effect!!
+ // WEAPON_SIGHT_OCCLUDED Don't block on this! Looks better for railings, etc.
+ )
+
+ //=========================================================
+ // AR2 Alt Fire Attack
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_AR2_ALTFIRE,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_ANNOUNCE_ATTACK 1"
+ " TASK_COMBINE_PLAY_SEQUENCE_FACE_ALTFIRE_TARGET ACTIVITY:ACT_COMBINE_AR2_ALTFIRE"
+ ""
+ " Interrupts"
+ )
+
+ //=========================================================
+ // Mapmaker forced grenade throw
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_FORCED_GRENADE_THROW,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_COMBINE_FACE_TOSS_DIR 0"
+ " TASK_ANNOUNCE_ATTACK 2" // 2 = grenade
+ " TASK_PLAY_SEQUENCE ACTIVITY:ACT_RANGE_ATTACK2"
+ " TASK_COMBINE_DEFER_SQUAD_GRENADES 0"
+ ""
+ " Interrupts"
+ )
+
+ //=========================================================
+ // Move to LOS of the mapmaker's forced grenade throw target
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_MOVE_TO_FORCED_GREN_LOS,
+
+ " Tasks "
+ " TASK_SET_TOLERANCE_DISTANCE 48"
+ " TASK_COMBINE_GET_PATH_TO_FORCED_GREN_LOS 0"
+ " TASK_SPEAK_SENTENCE 1"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " "
+ " Interrupts "
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK2"
+ " COND_HEAR_DANGER"
+ " COND_HEAR_MOVE_AWAY"
+ " COND_HEAVY_DAMAGE"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_RANGE_ATTACK2
+ //
+ // secondary range attack. Overriden because base class stops attacking when the enemy is occluded.
+ // combines's grenade toss requires the enemy be occluded.
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_RANGE_ATTACK2,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_COMBINE_FACE_TOSS_DIR 0"
+ " TASK_ANNOUNCE_ATTACK 2" // 2 = grenade
+ " TASK_PLAY_SEQUENCE ACTIVITY:ACT_RANGE_ATTACK2"
+ " TASK_COMBINE_DEFER_SQUAD_GRENADES 0"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_COMBINE_WAIT_IN_COVER" // don't run immediately after throwing grenade.
+ ""
+ " Interrupts"
+ )
+
+
+ //=========================================================
+ // Throw a grenade, then run off and reload.
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_GRENADE_AND_RELOAD,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_COMBINE_FACE_TOSS_DIR 0"
+ " TASK_ANNOUNCE_ATTACK 2" // 2 = grenade
+ " TASK_PLAY_SEQUENCE ACTIVITY:ACT_RANGE_ATTACK2"
+ " TASK_COMBINE_DEFER_SQUAD_GRENADES 0"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_HIDE_AND_RELOAD" // don't run immediately after throwing grenade.
+ ""
+ " Interrupts"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_PATROL,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_WANDER 900540"
+ " TASK_WALK_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_REASONABLE 0"
+ " TASK_WAIT 3"
+ " TASK_WAIT_RANDOM 3"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_COMBINE_PATROL" // keep doing it
+ ""
+ " Interrupts"
+ " COND_ENEMY_DEAD"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_HEAR_DANGER"
+ " COND_HEAR_MOVE_AWAY"
+ " COND_NEW_ENEMY"
+ " COND_SEE_ENEMY"
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_CAN_RANGE_ATTACK2"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_BUGBAIT_DISTRACTION,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_RESET_ACTIVITY 0"
+ " TASK_PLAY_SEQUENCE ACTIVITY:ACT_COMBINE_BUGBAIT"
+ ""
+ " Interrupts"
+ ""
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_CHARGE_TURRET
+ //
+ // Used to run straight at enemy turrets to knock them over.
+ // Prevents squadmates from throwing grenades during.
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_CHARGE_TURRET,
+
+ " Tasks"
+ " TASK_COMBINE_DEFER_SQUAD_GRENADES 0"
+ " TASK_STOP_MOVING 0"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY_FAILED"
+ " TASK_GET_CHASE_PATH_TO_ENEMY 300"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_FACE_ENEMY 0"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_ENEMY_UNREACHABLE"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK2"
+ " COND_TOO_CLOSE_TO_ATTACK"
+ " COND_TASK_FAILED"
+ " COND_LOST_ENEMY"
+ " COND_BETTER_WEAPON_AVAILABLE"
+ " COND_HEAR_DANGER"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_CHARGE_PLAYER
+ //
+ // Used to run straight at enemy player since physgun combat
+ // is more fun when the enemies are close
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_CHARGE_PLAYER,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY_FAILED"
+ " TASK_COMBINE_CHASE_ENEMY_CONTINUOUSLY 192"
+ " TASK_FACE_ENEMY 0"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_ENEMY_UNREACHABLE"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK2"
+ " COND_TASK_FAILED"
+ " COND_LOST_ENEMY"
+ " COND_HEAR_DANGER"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_DROP_GRENADE
+ //
+ // Place a grenade at my feet
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_DROP_GRENADE,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_PLAY_SEQUENCE ACTIVITY:ACT_SPECIAL_ATTACK2"
+ " TASK_FIND_COVER_FROM_ENEMY 99"
+ " TASK_FIND_FAR_NODE_COVER_FROM_ENEMY 384"
+ " TASK_CLEAR_MOVE_WAIT 0"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ ""
+ " Interrupts"
+ )
+
+ //=========================================================
+ // SCHED_COMBINE_PATROL_ENEMY
+ //
+ // Used instead if SCHED_COMBINE_PATROL if I have an enemy.
+ // Wait for the enemy a bit in the hopes of ambushing him.
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_PATROL_ENEMY,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_WAIT_FACE_ENEMY 1"
+ " TASK_WAIT_FACE_ENEMY_RANDOM 3"
+ ""
+ " Interrupts"
+ " COND_ENEMY_DEAD"
+ " COND_LIGHT_DAMAGE"
+ " COND_HEAVY_DAMAGE"
+ " COND_HEAR_DANGER"
+ " COND_HEAR_MOVE_AWAY"
+ " COND_NEW_ENEMY"
+ " COND_SEE_ENEMY"
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_CAN_RANGE_ATTACK2"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_BURNING_STAND,
+
+ " Tasks"
+ " TASK_SET_ACTIVITY ACTIVITY:ACT_COMBINE_BUGBAIT"
+ " TASK_RANDOMIZE_FRAMERATE 20"
+ " TASK_WAIT 2"
+ " TASK_WAIT_RANDOM 3"
+ " TASK_COMBINE_DIE_INSTANTLY DMG_BURN"
+ " TASK_WAIT 1.0"
+ " "
+ " Interrupts"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_FACE_IDEAL_YAW,
+
+ " Tasks"
+ " TASK_FACE_IDEAL 0"
+ " "
+ " Interrupts"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_COMBINE_MOVE_TO_MELEE,
+
+ " Tasks"
+ " TASK_STORE_ENEMY_POSITION_IN_SAVEPOSITION 0"
+ " TASK_GET_PATH_TO_SAVEPOSITION 0"
+ " TASK_RUN_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " "
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_CAN_MELEE_ATTACK1"
+ )
+
+ AI_END_CUSTOM_NPC()