aboutsummaryrefslogtreecommitdiff
path: root/mp/src/game/server/hl2/npc_stalker.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'mp/src/game/server/hl2/npc_stalker.cpp')
-rw-r--r--mp/src/game/server/hl2/npc_stalker.cpp1509
1 files changed, 1509 insertions, 0 deletions
diff --git a/mp/src/game/server/hl2/npc_stalker.cpp b/mp/src/game/server/hl2/npc_stalker.cpp
new file mode 100644
index 00000000..9fe62e22
--- /dev/null
+++ b/mp/src/game/server/hl2/npc_stalker.cpp
@@ -0,0 +1,1509 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+//=============================================================================//
+
+#include "cbase.h"
+#include "soundent.h"
+#include "game.h"
+#include "beam_shared.h"
+#include "Sprite.h"
+#include "npcevent.h"
+#include "npc_stalker.h"
+#include "ai_hull.h"
+#include "ai_default.h"
+#include "ai_node.h"
+#include "ai_network.h"
+#include "ai_hint.h"
+#include "ai_link.h"
+#include "ai_waypoint.h"
+#include "ai_navigator.h"
+#include "ai_senses.h"
+#include "ai_squadslot.h"
+#include "ai_squad.h"
+#include "ai_memory.h"
+#include "ai_tacticalservices.h"
+#include "ai_moveprobe.h"
+#include "npc_talker.h"
+#include "activitylist.h"
+#include "bitstring.h"
+#include "decals.h"
+#include "player.h"
+#include "entitylist.h"
+#include "ndebugoverlay.h"
+#include "ai_interactions.h"
+#include "animation.h"
+#include "scriptedtarget.h"
+#include "vstdlib/random.h"
+#include "engine/IEngineSound.h"
+#include "world.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+//#define STALKER_DEBUG
+#define MIN_STALKER_FIRE_RANGE 64
+#define MAX_STALKER_FIRE_RANGE 3600 // 3600 feet.
+#define STALKER_LASER_ATTACHMENT 1
+#define STALKER_TRIGGER_DIST 200 // Enemy dist. that wakes up the stalker
+#define STALKER_SENTENCE_VOLUME (float)0.35
+#define STALKER_LASER_DURATION 99999
+#define STALKER_LASER_RECHARGE 1
+#define STALKER_PLAYER_AGGRESSION 1
+
+enum StalkerBeamPower_e
+{
+ STALKER_BEAM_LOW,
+ STALKER_BEAM_MED,
+ STALKER_BEAM_HIGH,
+};
+
+//Animation events
+#define STALKER_AE_MELEE_HIT 1
+
+ConVar sk_stalker_health( "sk_stalker_health","0");
+ConVar sk_stalker_melee_dmg( "sk_stalker_melee_dmg","0");
+
+extern void SpawnBlood(Vector vecSpot, const Vector &vAttackDir, int bloodColor, float flDamage);
+
+LINK_ENTITY_TO_CLASS( npc_stalker, CNPC_Stalker );
+
+float g_StalkerBeamThinkTime = 0.0; //0.025;
+
+
+//=========================================================
+// Private activities.
+//=========================================================
+static int ACT_STALKER_WORK = 0;
+
+//=========================================================
+// Stalker schedules
+//=========================================================
+enum
+{
+ SCHED_STALKER_CHASE_ENEMY = LAST_SHARED_SCHEDULE,
+ SCHED_STALKER_RANGE_ATTACK,
+ SCHED_STALKER_ACQUIRE_PLAYER,
+ SCHED_STALKER_PATROL,
+};
+
+//=========================================================
+// Stalker Tasks
+//=========================================================
+enum
+{
+ TASK_STALKER_ZIGZAG = LAST_SHARED_TASK,
+ TASK_STALKER_SCREAM,
+};
+
+// -----------------------------------------------
+// > Squad slots
+// -----------------------------------------------
+enum SquadSlot_T
+{
+ SQUAD_SLOT_CHASE_ENEMY_1 = LAST_SHARED_SQUADSLOT,
+ SQUAD_SLOT_CHASE_ENEMY_2,
+};
+
+//-----------------------------------------------------------------------------
+
+BEGIN_DATADESC( CNPC_Stalker )
+
+ DEFINE_KEYFIELD( m_eBeamPower, FIELD_INTEGER, "BeamPower" ),
+ DEFINE_FIELD( m_vLaserDir, FIELD_VECTOR),
+ DEFINE_FIELD( m_vLaserTargetPos, FIELD_POSITION_VECTOR),
+ DEFINE_FIELD( m_fBeamEndTime, FIELD_FLOAT),
+ DEFINE_FIELD( m_fBeamRechargeTime, FIELD_FLOAT),
+ DEFINE_FIELD( m_fNextDamageTime, FIELD_FLOAT),
+ DEFINE_FIELD( m_bPlayingHitWall, FIELD_FLOAT),
+ DEFINE_FIELD( m_bPlayingHitFlesh, FIELD_FLOAT),
+ DEFINE_FIELD( m_pBeam, FIELD_CLASSPTR),
+ DEFINE_FIELD( m_pLightGlow, FIELD_CLASSPTR),
+ DEFINE_FIELD( m_flNextNPCThink, FIELD_FLOAT),
+ DEFINE_FIELD( m_vLaserCurPos, FIELD_POSITION_VECTOR),
+ DEFINE_FIELD( m_flNextAttackSoundTime, FIELD_TIME ),
+ DEFINE_FIELD( m_flNextBreatheSoundTime, FIELD_TIME ),
+ DEFINE_FIELD( m_flNextScrambleSoundTime, FIELD_TIME ),
+ DEFINE_FIELD( m_nextSmokeTime, FIELD_TIME ),
+ DEFINE_FIELD( m_iPlayerAggression, FIELD_INTEGER ),
+ DEFINE_FIELD( m_flNextScreamTime, FIELD_TIME ),
+
+ // Function Pointers
+ DEFINE_THINKFUNC( StalkerThink ),
+
+END_DATADESC()
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+int CNPC_Stalker::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo )
+{
+ CTakeDamageInfo info = inputInfo;
+
+ // --------------------------------------------
+ // Don't take a lot of damage from Vortigaunt
+ // --------------------------------------------
+ if (info.GetAttacker()->Classify() == CLASS_VORTIGAUNT)
+ {
+ info.ScaleDamage( 0.25 );
+ }
+
+
+ int ret = BaseClass::OnTakeDamage_Alive( info );
+
+ // If player shot me make sure I'm mad at him even if I wasn't earlier
+ if ( (info.GetAttacker()->GetFlags() & FL_CLIENT) )
+ {
+ AddClassRelationship( CLASS_PLAYER, D_HT, 0 );
+ }
+ return ret;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+float CNPC_Stalker::MaxYawSpeed( void )
+{
+#ifdef HL2_EPISODIC
+ return 10.0f;
+#else
+ switch( GetActivity() )
+ {
+ case ACT_TURN_LEFT:
+ case ACT_TURN_RIGHT:
+ return 160;
+ break;
+ case ACT_RUN:
+ case ACT_RUN_HURT:
+ return 280;
+ break;
+ default:
+ return 160;
+ break;
+ }
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//
+//
+// Output : int
+//-----------------------------------------------------------------------------
+Class_T CNPC_Stalker::Classify( void )
+{
+ return CLASS_STALKER;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+void CNPC_Stalker::PrescheduleThink()
+{
+ if (gpGlobals->curtime > m_flNextBreatheSoundTime)
+ {
+ EmitSound( "NPC_Stalker.Ambient01" );
+ m_flNextBreatheSoundTime = gpGlobals->curtime + 3.0 + random->RandomFloat( 0.0, 5.0 );
+ }
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+bool CNPC_Stalker::IsValidEnemy( CBaseEntity *pEnemy )
+{
+ Class_T enemyClass = pEnemy->Classify();
+
+ if( enemyClass == CLASS_PLAYER || enemyClass == CLASS_PLAYER_ALLY || enemyClass == CLASS_PLAYER_ALLY_VITAL )
+ {
+ // Don't get angry at these folks unless provoked.
+ if( m_iPlayerAggression < STALKER_PLAYER_AGGRESSION )
+ {
+ return false;
+ }
+ }
+
+ if( enemyClass == CLASS_BULLSEYE && pEnemy->GetParent() )
+ {
+ // This bullseye is in heirarchy with something. If that
+ // something is held by the physcannon, this bullseye is
+ // NOT a valid enemy.
+ IPhysicsObject *pPhys = pEnemy->GetParent()->VPhysicsGetObject();
+ if( pPhys && (pPhys->GetGameFlags() & FVPHYSICS_PLAYER_HELD) )
+ {
+ return false;
+ }
+ }
+
+ if( GetEnemy() && HasCondition(COND_SEE_ENEMY) )
+ {
+ // Short attention span. If I have an enemy, stick with it.
+ if( GetEnemy() != pEnemy )
+ {
+ return false;
+ }
+ }
+
+ if( IsStrategySlotRangeOccupied( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) && !HasStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ {
+ return false;
+ }
+
+ if( !FVisible(pEnemy) )
+ {
+ // Don't take an enemy you can't see. Since stalkers move way too slowly to
+ // establish line of fire, usually an enemy acquired by means other than
+ // the Stalker's own eyesight will always get away while the stalker plods
+ // slowly to their last known position. So don't take enemies you can't see.
+ return false;
+ }
+
+ return BaseClass::IsValidEnemy(pEnemy);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//
+//
+//-----------------------------------------------------------------------------
+void CNPC_Stalker::Spawn( void )
+{
+ Precache( );
+
+ SetModel( "models/stalker.mdl" );
+ SetHullType(HULL_HUMAN);
+ SetHullSizeNormal();
+
+ SetSolid( SOLID_BBOX );
+ AddSolidFlags( FSOLID_NOT_STANDABLE );
+ SetMoveType( MOVETYPE_STEP );
+ m_bloodColor = DONT_BLEED;
+ m_iHealth = sk_stalker_health.GetFloat();
+ m_flFieldOfView = 0.1;// indicates the width of this NPC's forward view cone ( as a dotproduct result )
+ m_NPCState = NPC_STATE_NONE;
+ CapabilitiesAdd( bits_CAP_SQUAD | bits_CAP_MOVE_GROUND );
+ CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1);
+
+ m_flNextAttackSoundTime = 0;
+ m_flNextBreatheSoundTime = gpGlobals->curtime + random->RandomFloat( 0.0, 10.0 );
+ m_flNextScrambleSoundTime = 0;
+ m_nextSmokeTime = 0;
+ m_bPlayingHitWall = false;
+ m_bPlayingHitFlesh = false;
+
+ m_fBeamEndTime = 0;
+ m_fBeamRechargeTime = 0;
+ m_fNextDamageTime = 0;
+
+ NPCInit();
+
+ m_flDistTooFar = MAX_STALKER_FIRE_RANGE;
+
+ m_iPlayerAggression = 0;
+
+ GetSenses()->SetDistLook(MAX_STALKER_FIRE_RANGE - 1);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//
+//
+//-----------------------------------------------------------------------------
+void CNPC_Stalker::Precache( void )
+{
+ PrecacheModel("models/stalker.mdl");
+ PrecacheModel("sprites/laser.vmt");
+
+ PrecacheModel("sprites/redglow1.vmt");
+ PrecacheModel("sprites/orangeglow1.vmt");
+ PrecacheModel("sprites/yellowglow1.vmt");
+
+ PrecacheScriptSound( "NPC_Stalker.BurnFlesh" );
+ PrecacheScriptSound( "NPC_Stalker.BurnWall" );
+ PrecacheScriptSound( "NPC_Stalker.FootstepLeft" );
+ PrecacheScriptSound( "NPC_Stalker.FootstepRight" );
+ PrecacheScriptSound( "NPC_Stalker.Hit" );
+ PrecacheScriptSound( "NPC_Stalker.Ambient01" );
+ PrecacheScriptSound( "NPC_Stalker.Scream" );
+ PrecacheScriptSound( "NPC_Stalker.Pain" );
+ PrecacheScriptSound( "NPC_Stalker.Die" );
+
+ BaseClass::Precache();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : -
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CNPC_Stalker::CreateBehaviors()
+{
+ AddBehavior( &m_ActBusyBehavior );
+
+ return BaseClass::CreateBehaviors();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+void CNPC_Stalker::IdleSound ( void )
+{
+}
+
+void CNPC_Stalker::OnScheduleChange()
+{
+ KillAttackBeam();
+
+ BaseClass::OnScheduleChange();
+}
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : pInflictor -
+// pAttacker -
+// flDamage -
+// bitsDamageType -
+//-----------------------------------------------------------------------------
+void CNPC_Stalker::Event_Killed( const CTakeDamageInfo &info )
+{
+ if( IsInSquad() && info.GetAttacker()->IsPlayer() )
+ {
+ AISquadIter_t iter;
+ for ( CAI_BaseNPC *pSquadMember = GetSquad()->GetFirstMember( &iter ); pSquadMember; pSquadMember = GetSquad()->GetNextMember( &iter ) )
+ {
+ if ( pSquadMember->IsAlive() && pSquadMember != this )
+ {
+ CNPC_Stalker *pStalker = dynamic_cast <CNPC_Stalker*>(pSquadMember);
+
+ if( pStalker && pStalker->FVisible(info.GetAttacker()) )
+ {
+ pStalker->m_iPlayerAggression++;
+ }
+ }
+ }
+ }
+
+ KillAttackBeam();
+ BaseClass::Event_Killed( info );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+void CNPC_Stalker::DeathSound( const CTakeDamageInfo &info )
+{
+ EmitSound( "NPC_Stalker.Die" );
+};
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+void CNPC_Stalker::PainSound( const CTakeDamageInfo &info )
+{
+ EmitSound( "NPC_Stalker.Pain" );
+ m_flNextScrambleSoundTime = gpGlobals->curtime + 1.5;
+ m_flNextBreatheSoundTime = gpGlobals->curtime + 1.5;
+ m_flNextAttackSoundTime = gpGlobals->curtime + 1.5;
+};
+
+//-----------------------------------------------------------------------------
+// Purpose: Translates squad slot positions into schedules
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+#if 0
+// @TODO (toml 07-18-03): this function is never called. Presumably what it is trying to do still needs to be done...
+int CNPC_Stalker::GetSlotSchedule(int slotID)
+{
+ switch (slotID)
+ {
+
+ case SQUAD_SLOT_CHASE_ENEMY_1:
+ case SQUAD_SLOT_CHASE_ENEMY_2:
+ return SCHED_STALKER_CHASE_ENEMY;
+ break;
+ }
+ return SCHED_NONE;
+}
+#endif
+
+
+void CNPC_Stalker::UpdateAttackBeam( void )
+{
+ CBaseEntity *pEnemy = GetEnemy();
+ // If not burning at a target
+ if (pEnemy)
+ {
+ if (gpGlobals->curtime > m_fBeamEndTime)
+ {
+ TaskComplete();
+ }
+ else
+ {
+ Vector enemyLKP = GetEnemyLKP();
+ m_vLaserTargetPos = enemyLKP + pEnemy->GetViewOffset();
+
+ // Face my enemy
+ GetMotor()->SetIdealYawToTargetAndUpdate( enemyLKP );
+
+ // ---------------------------------------------
+ // Get beam end point
+ // ---------------------------------------------
+ Vector vecSrc = LaserStartPosition(GetAbsOrigin());
+ Vector targetDir = m_vLaserTargetPos - vecSrc;
+ VectorNormalize(targetDir);
+ // --------------------------------------------------------
+ // If beam position and laser dir are way off, end attack
+ // --------------------------------------------------------
+ if ( DotProduct(targetDir,m_vLaserDir) < 0.5 )
+ {
+ TaskComplete();
+ return;
+ }
+
+ trace_t tr;
+ AI_TraceLine( vecSrc, vecSrc + m_vLaserDir * MAX_STALKER_FIRE_RANGE, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr);
+ // ---------------------------------------------
+ // If beam not long enough, stop attacking
+ // ---------------------------------------------
+ if (tr.fraction == 1.0)
+ {
+ TaskComplete();
+ return;
+ }
+
+ CSoundEnt::InsertSound(SOUND_DANGER, tr.endpos, 60, 0.025, this);
+ }
+ }
+ else
+ {
+ TaskFail(FAIL_NO_ENEMY);
+ }
+}
+
+//=========================================================
+// start task
+//=========================================================
+void CNPC_Stalker::StartTask( const Task_t *pTask )
+{
+ switch ( pTask->iTask )
+ {
+ case TASK_STALKER_SCREAM:
+ {
+ if( gpGlobals->curtime > m_flNextScreamTime )
+ {
+ EmitSound( "NPC_Stalker.Scream" );
+ m_flNextScreamTime = gpGlobals->curtime + random->RandomFloat( 10.0, 15.0 );
+ }
+
+ TaskComplete();
+ }
+
+ case TASK_ANNOUNCE_ATTACK:
+ {
+ // 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->FInViewCone ( this )) &&
+ (gpGlobals->curtime - m_flLastAttackTime > 1.0) )
+ {
+ m_flLastAttackTime = gpGlobals->curtime;
+
+ // Always play this sound
+ EmitSound( "NPC_Stalker.Scream" );
+ m_flNextScrambleSoundTime = gpGlobals->curtime + 2;
+ m_flNextBreatheSoundTime = gpGlobals->curtime + 2;
+
+ // Wait two seconds
+ SetWait( 2.0 );
+ SetActivity(ACT_IDLE);
+ }
+ break;
+ }
+ case TASK_STALKER_ZIGZAG:
+ break;
+ case TASK_RANGE_ATTACK1:
+ {
+ CBaseEntity *pEnemy = GetEnemy();
+ if (pEnemy)
+ {
+ m_vLaserTargetPos = GetEnemyLKP() + pEnemy->GetViewOffset();
+
+ // Never hit target on first try
+ Vector missPos = m_vLaserTargetPos;
+
+ if( pEnemy->Classify() == CLASS_BULLSEYE && hl2_episodic.GetBool() )
+ {
+ missPos.x += 60 + 120*random->RandomInt(-1,1);
+ missPos.y += 60 + 120*random->RandomInt(-1,1);
+ }
+ else
+ {
+ missPos.x += 80*random->RandomInt(-1,1);
+ missPos.y += 80*random->RandomInt(-1,1);
+ }
+
+ // ----------------------------------------------------------------------
+ // If target is facing me and not running towards me shoot below his feet
+ // so he can see the laser coming
+ // ----------------------------------------------------------------------
+ CBaseCombatCharacter *pBCC = ToBaseCombatCharacter(pEnemy);
+ if (pBCC)
+ {
+ Vector targetToMe = (pBCC->GetAbsOrigin() - GetAbsOrigin());
+ Vector vBCCFacing = pBCC->BodyDirection2D( );
+ if ((DotProduct(vBCCFacing,targetToMe) < 0) &&
+ (pBCC->GetSmoothedVelocity().Length() < 50))
+ {
+ missPos.z -= 150;
+ }
+ // --------------------------------------------------------
+ // If facing away or running towards laser,
+ // shoot above target's head
+ // --------------------------------------------------------
+ else
+ {
+ missPos.z += 60;
+ }
+ }
+ m_vLaserDir = missPos - LaserStartPosition(GetAbsOrigin());
+ VectorNormalize(m_vLaserDir);
+ }
+ else
+ {
+ TaskFail(FAIL_NO_ENEMY);
+ return;
+ }
+
+ StartAttackBeam();
+ SetActivity(ACT_RANGE_ATTACK1);
+ break;
+ }
+ case TASK_GET_PATH_TO_ENEMY_LOS:
+ {
+ if ( GetEnemy() != NULL )
+ {
+ BaseClass:: StartTask( pTask );
+ return;
+ }
+
+ Vector posLos;
+
+ if (GetTacticalServices()->FindLos(m_vLaserCurPos, m_vLaserCurPos, MIN_STALKER_FIRE_RANGE, MAX_STALKER_FIRE_RANGE, 1.0, &posLos))
+ {
+ AI_NavGoal_t goal( posLos, ACT_RUN, AIN_HULL_TOLERANCE );
+ GetNavigator()->SetGoal( goal );
+ }
+ else
+ {
+ TaskFail(FAIL_NO_SHOOT);
+ }
+ break;
+ }
+ case TASK_FACE_ENEMY:
+ {
+ if ( GetEnemy() != NULL )
+ {
+ BaseClass:: StartTask( pTask );
+ return;
+ }
+ GetMotor()->SetIdealYawToTarget( m_vLaserCurPos );
+ break;
+ }
+ default:
+ BaseClass:: StartTask( pTask );
+ break;
+ }
+}
+
+//=========================================================
+// RunTask
+//=========================================================
+void CNPC_Stalker::RunTask( const Task_t *pTask )
+{
+ switch ( pTask->iTask )
+ {
+ 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_STALKER_ZIGZAG :
+ {
+
+ if (GetNavigator()->GetGoalType() == GOALTYPE_NONE)
+ {
+ TaskComplete();
+ GetNavigator()->StopMoving(); // Stop moving
+ }
+ else if (!GetNavigator()->IsGoalActive())
+ {
+ SetIdealActivity( GetStoppedActivity() );
+ }
+ else if (ValidateNavGoal())
+ {
+ SetIdealActivity( GetNavigator()->GetMovementActivity() );
+ AddZigZagToPath();
+ }
+ break;
+ }
+ case TASK_RANGE_ATTACK1:
+ UpdateAttackBeam();
+ if ( !TaskIsRunning() || HasCondition( COND_TASK_FAILED ))
+ {
+ KillAttackBeam();
+ }
+ break;
+
+ case TASK_FACE_ENEMY:
+ {
+ if ( GetEnemy() != NULL )
+ {
+ BaseClass:: RunTask( pTask );
+ return;
+ }
+ GetMotor()->SetIdealYawToTargetAndUpdate( m_vLaserCurPos );
+
+ if ( FacingIdeal() )
+ {
+ TaskComplete();
+ }
+ break;
+ }
+ default:
+ {
+ BaseClass::RunTask( pTask );
+ break;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+int CNPC_Stalker::SelectSchedule( void )
+{
+ if ( BehaviorSelectSchedule() )
+ {
+ return BaseClass::SelectSchedule();
+ }
+
+ switch ( m_NPCState )
+ {
+ case NPC_STATE_IDLE:
+ case NPC_STATE_ALERT:
+ {
+ if( HasCondition(COND_IN_PVS) )
+ {
+ return SCHED_STALKER_PATROL;
+ }
+
+ return SCHED_IDLE_STAND;
+
+ break;
+ }
+
+ case NPC_STATE_COMBAT:
+ {
+ // -----------
+ // new enemy
+ // -----------
+ if( HasCondition( COND_NEW_ENEMY ) )
+ {
+ if( GetEnemy()->IsPlayer() )
+ {
+ return SCHED_STALKER_ACQUIRE_PLAYER;
+ }
+ }
+
+ if( HasCondition( COND_CAN_RANGE_ATTACK1 ) )
+ {
+ if( OccupyStrategySlotRange(SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2) )
+ {
+ return SCHED_RANGE_ATTACK1;
+ }
+ else
+ {
+ return SCHED_STALKER_PATROL;
+ }
+ }
+
+ if( !HasCondition(COND_SEE_ENEMY) )
+ {
+ return SCHED_STALKER_PATROL;
+ }
+
+ return SCHED_COMBAT_FACE;
+
+ break;
+ }
+ }
+
+ // no special cases here, call the base class
+ return BaseClass::SelectSchedule();
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+int CNPC_Stalker::TranslateSchedule( int scheduleType )
+{
+ switch( scheduleType )
+ {
+ case SCHED_RANGE_ATTACK1:
+ {
+ return SCHED_STALKER_RANGE_ATTACK;
+ }
+ case SCHED_FAIL_ESTABLISH_LINE_OF_FIRE:
+ {
+ return SCHED_COMBAT_STAND;
+ break;
+ }
+ case SCHED_FAIL_TAKE_COVER:
+ {
+ return SCHED_RUN_RANDOM;
+ break;
+ }
+ }
+
+ return BaseClass::TranslateSchedule( scheduleType );
+}
+
+//------------------------------------------------------------------------------
+// Purpose : Returns position of laser for any given position of the staler
+// Input :
+// Output :
+//------------------------------------------------------------------------------
+Vector CNPC_Stalker::LaserStartPosition(Vector vStalkerPos)
+{
+ // Get attachment position
+ Vector vAttachPos;
+ GetAttachment(STALKER_LASER_ATTACHMENT,vAttachPos);
+
+ // Now convert to vStalkerPos
+ vAttachPos = vAttachPos - GetAbsOrigin() + vStalkerPos;
+ return vAttachPos;
+}
+
+//------------------------------------------------------------------------------
+// Purpose : Calculate position of beam
+// Input :
+// Output :
+//------------------------------------------------------------------------------
+void CNPC_Stalker::CalcBeamPosition(void)
+{
+ Vector targetDir = m_vLaserTargetPos - LaserStartPosition(GetAbsOrigin());
+ VectorNormalize(targetDir);
+
+ // ---------------------------------------
+ // Otherwise if burning towards an enemy
+ // ---------------------------------------
+ if ( GetEnemy() )
+ {
+ // ---------------------------------------
+ // Integrate towards target position
+ // ---------------------------------------
+ float iRate = 0.95;
+
+ if( GetEnemy()->Classify() == CLASS_BULLSEYE )
+ {
+ // Seek bullseyes faster
+ iRate = 0.8;
+ }
+
+ m_vLaserDir.x = (iRate * m_vLaserDir.x + (1-iRate) * targetDir.x);
+ m_vLaserDir.y = (iRate * m_vLaserDir.y + (1-iRate) * targetDir.y);
+ m_vLaserDir.z = (iRate * m_vLaserDir.z + (1-iRate) * targetDir.z);
+ VectorNormalize( m_vLaserDir );
+
+ // -----------------------------------------
+ // Add time-coherent noise to the position
+ // Must be scaled with distance
+ // -----------------------------------------
+ float fTargetDist = (GetAbsOrigin() - m_vLaserTargetPos).Length();
+ float noiseScale = atan(0.2/fTargetDist);
+ float m_fNoiseModX = 5;
+ float m_fNoiseModY = 5;
+ float m_fNoiseModZ = 5;
+
+ m_vLaserDir.x += 5*noiseScale*sin(m_fNoiseModX * gpGlobals->curtime + m_fNoiseModX);
+ m_vLaserDir.y += 5*noiseScale*sin(m_fNoiseModY * gpGlobals->curtime + m_fNoiseModY);
+ m_vLaserDir.z += 5*noiseScale*sin(m_fNoiseModZ * gpGlobals->curtime + m_fNoiseModZ);
+ }
+}
+
+
+void CNPC_Stalker::StartAttackBeam( void )
+{
+ if ( m_fBeamEndTime > gpGlobals->curtime || m_fBeamRechargeTime > gpGlobals->curtime )
+ {
+ // UNDONE: Debug this and fix!?!?!
+ m_fBeamRechargeTime = gpGlobals->curtime;
+ }
+ // ---------------------------------------------
+ // If I don't have a beam yet, create one
+ // ---------------------------------------------
+ // UNDONE: Why would I ever have a beam already?!?!?!
+ if (!m_pBeam)
+ {
+ Vector vecSrc = LaserStartPosition(GetAbsOrigin());
+ trace_t tr;
+ AI_TraceLine ( vecSrc, vecSrc + m_vLaserDir * MAX_STALKER_FIRE_RANGE, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr);
+ if ( tr.fraction >= 1.0 )
+ {
+ // too far
+ TaskComplete();
+ return;
+ }
+
+ m_pBeam = CBeam::BeamCreate( "sprites/laser.vmt", 2.0 );
+ m_pBeam->PointEntInit( tr.endpos, this );
+ m_pBeam->SetEndAttachment( STALKER_LASER_ATTACHMENT );
+ m_pBeam->SetBrightness( 255 );
+ m_pBeam->SetNoise( 0 );
+
+ switch (m_eBeamPower)
+ {
+ case STALKER_BEAM_LOW:
+ m_pBeam->SetColor( 255, 0, 0 );
+ m_pLightGlow = CSprite::SpriteCreate( "sprites/redglow1.vmt", GetAbsOrigin(), FALSE );
+ break;
+ case STALKER_BEAM_MED:
+ m_pBeam->SetColor( 255, 50, 0 );
+ m_pLightGlow = CSprite::SpriteCreate( "sprites/orangeglow1.vmt", GetAbsOrigin(), FALSE );
+ break;
+ case STALKER_BEAM_HIGH:
+ m_pBeam->SetColor( 255, 150, 0 );
+ m_pLightGlow = CSprite::SpriteCreate( "sprites/yellowglow1.vmt", GetAbsOrigin(), FALSE );
+ break;
+ }
+
+ // ----------------------------
+ // Light myself in a red glow
+ // ----------------------------
+ m_pLightGlow->SetTransparency( kRenderGlow, 255, 200, 200, 0, kRenderFxNoDissipation );
+ m_pLightGlow->SetAttachment( this, 1 );
+ m_pLightGlow->SetBrightness( 255 );
+ m_pLightGlow->SetScale( 0.65 );
+
+#if 0
+ CBaseEntity *pEnemy = GetEnemy();
+ // --------------------------------------------------------
+ // Play start up sound - client should always hear this!
+ // --------------------------------------------------------
+ if (pEnemy != NULL && (pEnemy->IsPlayer()) )
+ {
+ EmitAmbientSound( 0, pEnemy->GetAbsOrigin(), "NPC_Stalker.AmbientLaserStart" );
+ }
+ else
+ {
+ EmitAmbientSound( 0, GetAbsOrigin(), "NPC_Stalker.AmbientLaserStart" );
+ }
+#endif
+ }
+
+ SetThink( &CNPC_Stalker::StalkerThink );
+
+ m_flNextNPCThink = GetNextThink();
+ SetNextThink( gpGlobals->curtime + g_StalkerBeamThinkTime );
+ m_fBeamEndTime = gpGlobals->curtime + STALKER_LASER_DURATION;
+}
+
+//------------------------------------------------------------------------------
+// Purpose : Update beam more often then regular NPC think so it doesn't
+// move so jumpily over the ground
+// Input :
+// Output :
+//------------------------------------------------------------------------------
+void CNPC_Stalker::StalkerThink(void)
+{
+ DrawAttackBeam();
+ if (gpGlobals->curtime >= m_flNextNPCThink)
+ {
+ NPCThink();
+ m_flNextNPCThink = GetNextThink();
+ }
+
+ if ( m_pBeam )
+ {
+ SetNextThink( gpGlobals->curtime + g_StalkerBeamThinkTime );
+
+ // sanity check?!
+ const Task_t *pTask = GetTask();
+ if ( !pTask || pTask->iTask != TASK_RANGE_ATTACK1 || !TaskIsRunning() )
+ {
+ KillAttackBeam();
+ }
+ }
+ else
+ {
+ DevMsg( 2, "In StalkerThink() but no stalker beam found?\n" );
+ SetNextThink( m_flNextNPCThink );
+ }
+}
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+void CNPC_Stalker::NotifyDeadFriend( CBaseEntity *pFriend )
+{
+ BaseClass::NotifyDeadFriend(pFriend);
+}
+
+void CNPC_Stalker::DoSmokeEffect( const Vector &position )
+{
+ if ( gpGlobals->curtime > m_nextSmokeTime )
+ {
+ m_nextSmokeTime = gpGlobals->curtime + 0.5;
+ UTIL_Smoke(position, random->RandomInt(5, 10), 10);
+ }
+}
+
+//------------------------------------------------------------------------------
+// Purpose : Draw attack beam and do damage / decals
+// Input :
+// Output :
+//------------------------------------------------------------------------------
+void CNPC_Stalker::DrawAttackBeam(void)
+{
+ if (!m_pBeam)
+ return;
+
+ // ---------------------------------------------
+ // Get beam end point
+ // ---------------------------------------------
+ Vector vecSrc = LaserStartPosition(GetAbsOrigin());
+ trace_t tr;
+ AI_TraceLine( vecSrc, vecSrc + m_vLaserDir * MAX_STALKER_FIRE_RANGE, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr);
+
+ CalcBeamPosition();
+
+ bool bInWater = (UTIL_PointContents ( tr.endpos ) & MASK_WATER)?true:false;
+ // ---------------------------------------------
+ // Update the beam position
+ // ---------------------------------------------
+ m_pBeam->SetStartPos( tr.endpos );
+ m_pBeam->RelinkBeam();
+
+ Vector vAttachPos;
+ GetAttachment(STALKER_LASER_ATTACHMENT,vAttachPos);
+
+ Vector vecAimDir = tr.endpos - vAttachPos;
+ VectorNormalize( vecAimDir );
+
+ SetAim( vecAimDir );
+
+ // --------------------------------------------
+ // Play burn sounds
+ // --------------------------------------------
+ CBaseCombatCharacter *pBCC = ToBaseCombatCharacter( tr.m_pEnt );
+ if (pBCC)
+ {
+ if (gpGlobals->curtime > m_fNextDamageTime)
+ {
+ ClearMultiDamage();
+
+ float damage = 0.0;
+ switch (m_eBeamPower)
+ {
+ case STALKER_BEAM_LOW:
+ damage = 1;
+ break;
+ case STALKER_BEAM_MED:
+ damage = 3;
+ break;
+ case STALKER_BEAM_HIGH:
+ damage = 10;
+ break;
+ }
+
+ CTakeDamageInfo info( this, this, damage, DMG_SHOCK );
+ CalculateMeleeDamageForce( &info, m_vLaserDir, tr.endpos );
+ pBCC->DispatchTraceAttack( info, m_vLaserDir, &tr );
+ ApplyMultiDamage();
+ m_fNextDamageTime = gpGlobals->curtime + 0.1;
+ }
+ if (pBCC->Classify()!=CLASS_BULLSEYE)
+ {
+ if (!m_bPlayingHitFlesh)
+ {
+ CPASAttenuationFilter filter( m_pBeam,"NPC_Stalker.BurnFlesh" );
+ filter.MakeReliable();
+
+ EmitSound( filter, m_pBeam->entindex(),"NPC_Stalker.BurnFlesh" );
+ m_bPlayingHitFlesh = true;
+ }
+ if (m_bPlayingHitWall)
+ {
+ StopSound( m_pBeam->entindex(), "NPC_Stalker.BurnWall" );
+ m_bPlayingHitWall = false;
+ }
+
+ tr.endpos.z -= 24.0f;
+ if (!bInWater)
+ {
+ DoSmokeEffect(tr.endpos + tr.plane.normal * 8);
+ }
+ }
+ }
+
+ if (!pBCC || pBCC->Classify()==CLASS_BULLSEYE)
+ {
+ if (!m_bPlayingHitWall)
+ {
+ CPASAttenuationFilter filter( m_pBeam, "NPC_Stalker.BurnWall" );
+ filter.MakeReliable();
+
+ EmitSound( filter, m_pBeam->entindex(), "NPC_Stalker.BurnWall" );
+ m_bPlayingHitWall = true;
+ }
+ if (m_bPlayingHitFlesh)
+ {
+ StopSound(m_pBeam->entindex(), "NPC_Stalker.BurnFlesh" );
+ m_bPlayingHitFlesh = false;
+ }
+
+ UTIL_DecalTrace( &tr, "RedGlowFade");
+ UTIL_DecalTrace( &tr, "FadingScorch" );
+
+ tr.endpos.z -= 24.0f;
+ if (!bInWater)
+ {
+ DoSmokeEffect(tr.endpos + tr.plane.normal * 8);
+ }
+ }
+
+ if (bInWater)
+ {
+ UTIL_Bubbles(tr.endpos-Vector(3,3,3),tr.endpos+Vector(3,3,3),10);
+ }
+
+ /*
+ CBroadcastRecipientFilter filter;
+ TE_DynamicLight( filter, 0.0, EyePosition(), 255, 0, 0, 5, 0.2, 0 );
+ */
+}
+
+//------------------------------------------------------------------------------
+// Purpose : Draw attack beam and do damage / decals
+// Input :
+// Output :
+//------------------------------------------------------------------------------
+void CNPC_Stalker::KillAttackBeam(void)
+{
+ if ( !m_pBeam )
+ return;
+
+ // Kill sound
+ StopSound(m_pBeam->entindex(), "NPC_Stalker.BurnWall" );
+ StopSound(m_pBeam->entindex(), "NPC_Stalker.BurnFlesh" );
+
+ UTIL_Remove( m_pLightGlow );
+ UTIL_Remove( m_pBeam);
+ m_pBeam = NULL;
+ m_bPlayingHitWall = false;
+ m_bPlayingHitFlesh = false;
+
+ SetThink(&CNPC_Stalker::CallNPCThink);
+ if ( m_flNextNPCThink > gpGlobals->curtime )
+ {
+ SetNextThink( m_flNextNPCThink );
+ }
+
+ // Beam has to recharge
+ m_fBeamRechargeTime = gpGlobals->curtime + STALKER_LASER_RECHARGE;
+
+ ClearCondition( COND_CAN_RANGE_ATTACK1 );
+
+ RelaxAim();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Override so can handle LOS to m_pScriptedTarget
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+bool CNPC_Stalker::InnateWeaponLOSCondition( const Vector &ownerPos, const Vector &targetPos, bool bSetConditions )
+{
+ // --------------------
+ // Check for occlusion
+ // --------------------
+ // Base class version assumes innate weapon position is at eye level
+ Vector barrelPos = LaserStartPosition(ownerPos);
+ trace_t tr;
+ AI_TraceLine( barrelPos, targetPos, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr);
+
+ if ( tr.fraction == 1.0 )
+ {
+ return true;
+ }
+
+ CBaseEntity *pBE = tr.m_pEnt;
+ CBaseCombatCharacter *pBCC = ToBaseCombatCharacter( pBE );
+ if ( pBE == GetEnemy() )
+ {
+ return true;
+ }
+ else if (pBCC)
+ {
+ if (IRelationType( pBCC ) == D_HT)
+ {
+ return true;
+ }
+ else if (bSetConditions)
+ {
+ SetCondition(COND_WEAPON_BLOCKED_BY_FRIEND);
+ }
+ }
+ else if (bSetConditions)
+ {
+ SetCondition(COND_WEAPON_SIGHT_OCCLUDED);
+ SetEnemyOccluder(pBE);
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: For innate melee attack
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+int CNPC_Stalker::MeleeAttack1Conditions ( float flDot, float flDist )
+{
+ if (flDist > MIN_STALKER_FIRE_RANGE)
+ {
+ return COND_TOO_FAR_TO_ATTACK;
+ }
+ else if (flDot < 0.7)
+ {
+ return COND_NOT_FACING_ATTACK;
+ }
+ return COND_CAN_MELEE_ATTACK1;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: For innate range attack
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+int CNPC_Stalker::RangeAttack1Conditions( float flDot, float flDist )
+{
+ if (gpGlobals->curtime < m_fBeamRechargeTime )
+ {
+ return COND_NONE;
+ }
+
+ if( IsStrategySlotRangeOccupied( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) )
+ {
+ // Couldn't attack if I wanted to.
+ return COND_NONE;
+ }
+
+ if (flDist <= MIN_STALKER_FIRE_RANGE)
+ {
+ return COND_TOO_CLOSE_TO_ATTACK;
+ }
+ else if (flDist > (MAX_STALKER_FIRE_RANGE * 0.66f) )
+ {
+ return COND_TOO_FAR_TO_ATTACK;
+ }
+ else if (flDot < 0.7)
+ {
+ return COND_NOT_FACING_ATTACK;
+ }
+ return COND_CAN_RANGE_ATTACK1;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Catch stalker specific messages
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+void CNPC_Stalker::HandleAnimEvent( animevent_t *pEvent )
+{
+ switch( pEvent->event )
+ {
+ case NPC_EVENT_LEFTFOOT:
+ {
+ EmitSound( "NPC_Stalker.FootstepLeft", pEvent->eventtime );
+ }
+ break;
+ case NPC_EVENT_RIGHTFOOT:
+ {
+ EmitSound( "NPC_Stalker.FootstepRight", pEvent->eventtime );
+ }
+ break;
+
+ case STALKER_AE_MELEE_HIT:
+ {
+ CBaseEntity *pHurt;
+
+ pHurt = CheckTraceHullAttack( 32, Vector(-16,-16,-16), Vector(16,16,16), sk_stalker_melee_dmg.GetFloat(), DMG_SLASH );
+
+ if ( pHurt )
+ {
+ if ( pHurt->GetFlags() & (FL_NPC|FL_CLIENT) )
+ {
+ pHurt->ViewPunch( QAngle( 5, 0, random->RandomInt(-10,10)) );
+ }
+
+ // Spawn some extra blood if we hit a BCC
+ CBaseCombatCharacter* pBCC = ToBaseCombatCharacter( pHurt );
+ if (pBCC)
+ {
+ SpawnBlood(pBCC->EyePosition(), g_vecAttackDir, pBCC->BloodColor(), sk_stalker_melee_dmg.GetFloat());
+ }
+
+ // Play a attack hit sound
+ EmitSound( "NPC_Stalker.Hit" );
+ }
+ break;
+ }
+ default:
+ BaseClass::HandleAnimEvent( pEvent );
+ break;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Tells use whether or not the NPC cares about a given type of hint node.
+// Input : sHint -
+// Output : TRUE if the NPC is interested in this hint type, FALSE if not.
+//-----------------------------------------------------------------------------
+bool CNPC_Stalker::FValidateHintType(CAI_Hint *pHint)
+{
+ return(pHint->HintType() == HINT_WORLD_WORK_POSITION);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Override in subclasses to associate specific hint types
+// with activities
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+Activity CNPC_Stalker::GetHintActivity( short sHintType, Activity HintsActivity )
+{
+ if (sHintType == HINT_WORLD_WORK_POSITION)
+ {
+ return ( Activity )ACT_STALKER_WORK;
+ }
+
+ return BaseClass::GetHintActivity( sHintType, HintsActivity );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Override in subclasses to give specific hint types delays
+// before they can be used again
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+float CNPC_Stalker::GetHintDelay( short sHintType )
+{
+ if (sHintType == HINT_WORLD_WORK_POSITION)
+ {
+ return 2.0;
+ }
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+#define ZIG_ZAG_SIZE 3600
+
+void CNPC_Stalker::AddZigZagToPath(void)
+{
+ // If already on a detour don't add a zigzag
+ if (GetNavigator()->GetCurWaypointFlags() & bits_WP_TO_DETOUR)
+ {
+ return;
+ }
+
+ // If enemy isn't facing me or occluded, don't add a zigzag
+ if (HasCondition(COND_ENEMY_OCCLUDED) || !HasCondition ( COND_ENEMY_FACING_ME ))
+ {
+ return;
+ }
+
+ Vector waypointPos = GetNavigator()->GetCurWaypointPos();
+ Vector waypointDir = (waypointPos - GetAbsOrigin());
+
+ // If the distance to the next node is greater than ZIG_ZAG_SIZE
+ // then add a random zig/zag to the path
+ if (waypointDir.LengthSqr() > ZIG_ZAG_SIZE)
+ {
+ // Pick a random distance for the zigzag (less that sqrt(ZIG_ZAG_SIZE)
+ float distance = random->RandomFloat( 30, 60 );
+
+ // Get me a vector orthogonal to the direction of motion
+ VectorNormalize( waypointDir );
+ Vector vDirUp(0,0,1);
+ Vector vDir;
+ CrossProduct( waypointDir, vDirUp, vDir);
+
+ // Pick a random direction (left/right) for the zigzag
+ if (random->RandomInt(0,1))
+ {
+ vDir = -1 * vDir;
+ }
+
+ // Get zigzag position in direction of target waypoint
+ Vector zigZagPos = GetAbsOrigin() + waypointDir * 60;
+
+ // Now offset
+ zigZagPos = zigZagPos + (vDir * distance);
+
+ // Now make sure that we can still get to the zigzag position and the waypoint
+ AIMoveTrace_t moveTrace1, moveTrace2;
+ GetMoveProbe()->MoveLimit( NAV_GROUND, GetAbsOrigin(), zigZagPos, MASK_NPCSOLID, NULL, &moveTrace1);
+ GetMoveProbe()->MoveLimit( NAV_GROUND, zigZagPos, waypointPos, MASK_NPCSOLID, NULL, &moveTrace2);
+ if ( !IsMoveBlocked( moveTrace1 ) && !IsMoveBlocked( moveTrace2 ) )
+ {
+ GetNavigator()->PrependWaypoint( zigZagPos, NAV_GROUND, bits_WP_TO_DETOUR );
+ }
+ }
+}
+
+//------------------------------------------------------------------------------
+// Purpose :
+// Input :
+// Output :
+//------------------------------------------------------------------------------
+CNPC_Stalker::CNPC_Stalker(void)
+{
+#ifdef _DEBUG
+ m_vLaserDir.Init();
+ m_vLaserTargetPos.Init();
+ m_vLaserCurPos.Init();
+#endif
+}
+
+
+//------------------------------------------------------------------------------
+//
+// Schedules
+//
+//------------------------------------------------------------------------------
+
+AI_BEGIN_CUSTOM_NPC( npc_stalker, CNPC_Stalker )
+
+ DECLARE_TASK(TASK_STALKER_ZIGZAG)
+ DECLARE_TASK(TASK_STALKER_SCREAM)
+
+ DECLARE_ACTIVITY(ACT_STALKER_WORK)
+
+ DECLARE_SQUADSLOT(SQUAD_SLOT_CHASE_ENEMY_1)
+ DECLARE_SQUADSLOT(SQUAD_SLOT_CHASE_ENEMY_2)
+
+
+ //=========================================================
+ // > SCHED_STALKER_RANGE_ATTACK
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_STALKER_RANGE_ATTACK,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_RANGE_ATTACK1 0"
+ ""
+ " Interrupts"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_HEAVY_DAMAGE"
+ " COND_REPEATED_DAMAGE"
+ " COND_HEAR_DANGER"
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_ENEMY_OCCLUDED" // Don't break on this. Keep shooting at last location
+ )
+
+ //=========================================================
+ // > SCHED_STALKER_CHASE_ENEMY
+ //=========================================================
+ DEFINE_SCHEDULE
+ (
+ SCHED_STALKER_CHASE_ENEMY,
+
+ " Tasks"
+ " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY_FAILED"
+ " TASK_SET_TOLERANCE_DISTANCE 24"
+ " TASK_GET_PATH_TO_ENEMY 0"
+ " TASK_RUN_PATH 0"
+ " TASK_STALKER_ZIGZAG 0"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_ENEMY_DEAD"
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_CAN_MELEE_ATTACK1"
+ " COND_CAN_RANGE_ATTACK2"
+ " COND_CAN_MELEE_ATTACK2"
+ " COND_TOO_CLOSE_TO_ATTACK"
+ " COND_TASK_FAILED"
+ " COND_HEAR_DANGER"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_STALKER_ACQUIRE_PLAYER,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_ENEMY 0"
+ " TASK_WAIT_RANDOM 0.5"
+ " TASK_STALKER_SCREAM 0"
+ " TASK_WAIT 0.5"
+ " TASK_WAIT_RANDOM 0.5"
+ ""
+ " Interrupts"
+ )
+
+ DEFINE_SCHEDULE
+ (
+ SCHED_STALKER_PATROL,
+
+ " Tasks"
+ " TASK_STOP_MOVING 0"
+ " TASK_WAIT 0.5"// This makes them look a bit more vigilant, instead of INSTANTLY patrolling after some other action.
+ " TASK_WAIT_RANDOM 0.5"
+ " TASK_WANDER 18000600"
+ " TASK_FACE_PATH 0"
+ " TASK_WALK_PATH 0"
+ " TASK_WAIT_FOR_MOVEMENT 0"
+ " TASK_STOP_MOVING 0"
+ " TASK_FACE_REASONABLE 0"
+ " TASK_SET_SCHEDULE SCHEDULE:SCHED_STALKER_PATROL"
+ ""
+ " Interrupts"
+ " COND_NEW_ENEMY"
+ " COND_CAN_RANGE_ATTACK1"
+ " COND_SEE_ENEMY"
+ )
+
+AI_END_CUSTOM_NPC()