source-engine/game/server/hl2/npc_stalker.cpp
FluorescentCIAAfricanAmerican 3bf9df6b27 1
2020-04-22 12:56:21 -04:00

1510 lines
39 KiB
C++

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