You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3496 lines
91 KiB
3496 lines
91 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
//=============================================================================// |
|
|
|
#include "cbase.h" |
|
#include "ai_default.h" |
|
#include "ai_basenpc.h" |
|
#include "ammodef.h" |
|
#include "ai_task.h" |
|
#include "ai_schedule.h" |
|
#include "ai_node.h" |
|
#include "ai_hull.h" |
|
#include "ai_memory.h" |
|
#include "ai_senses.h" |
|
#include "beam_shared.h" |
|
#include "game.h" |
|
#include "npcevent.h" |
|
#include "entitylist.h" |
|
#include "activitylist.h" |
|
#include "soundent.h" |
|
#include "gib.h" |
|
#include "ndebugoverlay.h" |
|
#include "smoke_trail.h" |
|
#include "weapon_rpg.h" |
|
#include "player.h" |
|
#include "mathlib/mathlib.h" |
|
#include "vstdlib/random.h" |
|
#include "engine/IEngineSound.h" |
|
#include "IEffects.h" |
|
#include "effect_color_tables.h" |
|
#include "npc_rollermine.h" |
|
#include "eventqueue.h" |
|
|
|
#include "effect_dispatch_data.h" |
|
#include "te_effect_dispatch.h" |
|
|
|
#include "collisionutils.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
extern Vector PointOnLineNearestPoint(const Vector& vStartPos, const Vector& vEndPos, const Vector& vPoint); |
|
|
|
ConVar bulletSpeed( "bulletspeed", "6000" ); |
|
ConVar sniperLines( "showsniperlines", "0" ); |
|
ConVar sniperviewdist("sniperviewdist", "35" ); |
|
ConVar showsniperdist("showsniperdist", "0" ); |
|
ConVar sniperspeak( "sniperspeak", "0" ); |
|
ConVar sniper_xbox_delay( "sniper_xbox_delay", "1" ); |
|
|
|
// Moved to HL2_SharedGameRules because these are referenced by shared AmmoDef functions |
|
extern ConVar sk_dmg_sniper_penetrate_plr; |
|
extern ConVar sk_dmg_sniper_penetrate_npc; |
|
|
|
// No model, impervious to damage. |
|
#define SF_SNIPER_HIDDEN (1 << 16) |
|
#define SF_SNIPER_VIEWCONE (1 << 17) ///< when set, sniper only sees in a small cone around the laser. |
|
#define SF_SNIPER_NOCORPSE (1 << 18) ///< when set, no corpse |
|
#define SF_SNIPER_STARTDISABLED (1 << 19) |
|
#define SF_SNIPER_FAST (1 << 20) ///< This is faster-shooting sniper. Paint time is decreased 25%. Bullet speed increases 150%. |
|
#define SF_SNIPER_NOSWEEP (1 << 21) ///< This sniper doesn't sweep to the target or use decoys. |
|
|
|
// If the last time I fired at someone was between 0 and this many seconds, draw |
|
// a bead on them much faster. (use subsequent paint time) |
|
#define SNIPER_FASTER_ATTACK_PERIOD 3.0f |
|
|
|
// These numbers determine the interval between shots. They used to be constants, |
|
// but are now keyfields. HL2 backwards compatibility was maintained by supplying |
|
// default values in the constructor. |
|
#if 0 |
|
// How long to aim at someone before shooting them. |
|
#define SNIPER_PAINT_ENEMY_TIME 1.0f |
|
// ...plus this |
|
#define SNIPER_PAINT_NPC_TIME_NOISE 0.75f |
|
#else |
|
// How long to aim at someone before shooting them. |
|
#define SNIPER_DEFAULT_PAINT_ENEMY_TIME 1.0f |
|
// ...plus this |
|
#define SNIPER_DEFAULT_PAINT_NPC_TIME_NOISE 0.75f |
|
#endif |
|
|
|
#define SNIPER_SUBSEQUENT_PAINT_TIME ( ( IsXbox() ) ? 1.0f : 0.4f ) |
|
|
|
#define SNIPER_FOG_PAINT_ENEMY_TIME 0.25f |
|
#define SNIPER_PAINT_DECOY_TIME 2.0f |
|
#define SNIPER_PAINT_FRUSTRATED_TIME 1.0f |
|
#define SNIPER_QUICKAIM_TIME 0.2f |
|
#define SNIPER_PAINT_NO_SHOT_TIME 0.7f |
|
|
|
#define SNIPER_DECOY_MAX_MASS 200.0f |
|
|
|
// #def'ing this will turn on heaps of sniper debug messages. |
|
#undef SNIPER_DEBUG |
|
|
|
// Target protection |
|
#define SNIPER_PROTECTION_MINDIST (1024.0*1024.0) // Distance around protect target that sniper does priority modification in |
|
#define SNIPER_PROTECTION_PRIORITYCAP 100.0 // Max addition to priority of an enemy right next to the protect target, falls to 0 at SNIPER_PROTECTION_MINDIST. |
|
|
|
//--------------------------------------------------------- |
|
// Like an infotarget, but shares a spawnflag that has |
|
// relevance to the sniper. |
|
//--------------------------------------------------------- |
|
#define SF_SNIPERTARGET_SHOOTME 1 |
|
#define SF_SNIPERTARGET_NOINTERRUPT 2 |
|
#define SF_SNIPERTARGET_SNAPSHOT 4 |
|
#define SF_SNIPERTARGET_RESUME 8 |
|
#define SF_SNIPERTARGET_SNAPTO 16 |
|
#define SF_SNIPERTARGET_FOCUS 32 |
|
|
|
|
|
#define SNIPER_DECOY_RADIUS 256 |
|
#define SNIPER_NUM_DECOYS 5 |
|
|
|
#define NUM_OLDDECOYS 5 |
|
|
|
#define NUM_PENETRATIONS 3 |
|
|
|
#define PENETRATION_THICKNESS 5 |
|
|
|
#define SNIPER_MAX_GROUP_TARGETS 16 |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
class CSniperTarget : public CPointEntity |
|
{ |
|
DECLARE_DATADESC(); |
|
public: |
|
DECLARE_CLASS( CSniperTarget, CPointEntity ); |
|
|
|
bool KeyValue( const char *szKeyName, const char *szValue ); |
|
|
|
string_t m_iszGroupName; |
|
}; |
|
|
|
//--------------------------------------------------------- |
|
// Save/Restore |
|
//--------------------------------------------------------- |
|
BEGIN_DATADESC( CSniperTarget ) |
|
|
|
DEFINE_FIELD( m_iszGroupName, FIELD_STRING ), |
|
|
|
END_DATADESC() |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
class CSniperBullet : public CBaseEntity |
|
{ |
|
public: |
|
DECLARE_CLASS( CSniperBullet, CBaseEntity ); |
|
|
|
CSniperBullet( void ) { Init(); } |
|
|
|
Vector m_vecDir; |
|
|
|
Vector m_vecStart; |
|
Vector m_vecEnd; |
|
|
|
float m_flLastThink; |
|
float m_SoundTime; |
|
int m_AmmoType; |
|
int m_PenetratedAmmoType; |
|
float m_Speed; |
|
bool m_bDirectShot; |
|
|
|
void Precache( void ); |
|
bool IsActive( void ) { return m_fActive; } |
|
|
|
bool Start( const Vector &vecOrigin, const Vector &vecTarget, CBaseEntity *pOwner, bool bDirectShot ); |
|
void Stop( void ); |
|
|
|
void BulletThink( void ); |
|
|
|
void Init( void ); |
|
|
|
DECLARE_DATADESC(); |
|
|
|
private: |
|
|
|
// Only one shot per sniper at a time. If a bullet hasn't |
|
// hit, the shooter must wait. |
|
bool m_fActive; |
|
|
|
// This tracks how many times this single bullet has |
|
// struck. This is for penetration, so the bullet can |
|
// go through things. |
|
int m_iImpacts; |
|
}; |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
class CProtoSniper : public CAI_BaseNPC |
|
{ |
|
DECLARE_CLASS( CProtoSniper, CAI_BaseNPC ); |
|
|
|
public: |
|
CProtoSniper( void ); |
|
void Precache( void ); |
|
void Spawn( void ); |
|
Class_T Classify( void ); |
|
float MaxYawSpeed( void ); |
|
Vector EyePosition( void ); |
|
|
|
void UpdateEfficiency( bool bInPVS ) { SetEfficiency( ( GetSleepState() != AISS_AWAKE ) ? AIE_DORMANT : AIE_NORMAL ); SetMoveEfficiency( AIME_NORMAL ); } |
|
|
|
bool IsLaserOn( void ) { return m_pBeam != NULL; } |
|
|
|
void Event_Killed( const CTakeDamageInfo &info ); |
|
void Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ); |
|
void UpdateOnRemove( void ); |
|
int OnTakeDamage_Alive( const CTakeDamageInfo &info ); |
|
bool WeaponLOSCondition(const Vector &ownerPos, const Vector &targetPos, bool bSetConditions) {return true;} |
|
int IRelationPriority( CBaseEntity *pTarget ); |
|
bool IsFastSniper() { return HasSpawnFlags(SF_SNIPER_FAST); } |
|
|
|
bool QuerySeeEntity( CBaseEntity *pEntity, bool bOnlyHateOrFearIfNPC = false ); |
|
|
|
virtual bool FInViewCone( CBaseEntity *pEntity ); |
|
|
|
void StartTask( const Task_t *pTask ); |
|
void RunTask( const Task_t *pTask ); |
|
int RangeAttack1Conditions ( float flDot, float flDist ); |
|
bool FireBullet( const Vector &vecTarget, bool bDirectShot ); |
|
float GetBulletSpeed(); |
|
Vector DesiredBodyTarget( CBaseEntity *pTarget ); |
|
Vector LeadTarget( CBaseEntity *pTarget ); |
|
CBaseEntity *PickDeadPlayerTarget(); |
|
|
|
virtual int SelectSchedule( void ); |
|
virtual int TranslateSchedule( int scheduleType ); |
|
|
|
bool KeyValue( const char *szKeyName, const char *szValue ); |
|
|
|
void PrescheduleThink( void ); |
|
|
|
static const char *pAttackSounds[]; |
|
|
|
bool FCanCheckAttacks ( void ); |
|
bool FindDecoyObject( void ); |
|
|
|
void ScopeGlint(); |
|
|
|
int GetSoundInterests( void ); |
|
void OnListened(); |
|
|
|
Vector GetBulletOrigin( void ); |
|
|
|
virtual int Restore( IRestore &restore ); |
|
|
|
virtual void OnScheduleChange( void ); |
|
|
|
bool FVisible( CBaseEntity *pEntity, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ); |
|
|
|
bool ShouldNotDistanceCull() { return true; } |
|
|
|
int DrawDebugTextOverlays(); |
|
|
|
void NotifyShotMissedTarget(); |
|
|
|
private: |
|
|
|
bool ShouldSnapShot( void ); |
|
void ClearTargetGroup( void ); |
|
|
|
float GetPositionParameter( float flTime, bool fLinear ); |
|
|
|
void GetPaintAim( const Vector &vecStart, const Vector &vecGoal, float flParameter, Vector *pProgress ); |
|
|
|
bool IsSweepingRandomly( void ) { return m_iNumGroupTargets > 0; } |
|
|
|
void ClearOldDecoys( void ); |
|
void AddOldDecoy( CBaseEntity *pDecoy ); |
|
bool HasOldDecoy( CBaseEntity *pDecoy ); |
|
bool FindFrustratedShot( float flNoise ); |
|
|
|
bool VerifyShot( CBaseEntity *pTarget ); |
|
|
|
void SetSweepTarget( const char *pszTarget ); |
|
|
|
// Inputs |
|
void InputEnableSniper( inputdata_t &inputdata ); |
|
void InputDisableSniper( inputdata_t &inputdata ); |
|
void InputSetDecoyRadius( inputdata_t &inputdata ); |
|
void InputSweepTarget( inputdata_t &inputdata ); |
|
void InputSweepTargetHighestPriority( inputdata_t &inputdata ); |
|
void InputSweepGroupRandomly( inputdata_t &inputdata ); |
|
void InputStopSweeping( inputdata_t &inputdata ); |
|
void InputProtectTarget( inputdata_t &inputdata ); |
|
|
|
#if HL2_EPISODIC |
|
void InputSetPaintInterval( inputdata_t &inputdata ); |
|
void InputSetPaintIntervalVariance( inputdata_t &inputdata ); |
|
#endif |
|
|
|
void LaserOff( void ); |
|
void LaserOn( const Vector &vecTarget, const Vector &vecDeviance ); |
|
|
|
void PaintTarget( const Vector &vecTarget, float flPaintTime ); |
|
|
|
bool IsPlayerAllySniper(); |
|
|
|
private: |
|
|
|
/// This is the variable from which m_flPaintTime gets set. |
|
/// How long to aim at someone before shooting them. |
|
float m_flKeyfieldPaintTime; |
|
|
|
/// A random number from 0 to this is added to m_flKeyfieldPaintTime |
|
/// to yield m_flPaintTime's initial delay. |
|
float m_flKeyfieldPaintTimeNoise; |
|
|
|
// This keeps track of the last spot the laser painted. For |
|
// continuous sweeping that changes direction. |
|
Vector m_vecPaintCursor; |
|
float m_flPaintTime; |
|
|
|
bool m_fWeaponLoaded; |
|
bool m_fEnabled; |
|
bool m_fIsPatient; |
|
float m_flPatience; |
|
int m_iMisses; |
|
EHANDLE m_hDecoyObject; |
|
EHANDLE m_hSweepTarget; |
|
Vector m_vecDecoyObjectTarget; |
|
Vector m_vecFrustratedTarget; |
|
Vector m_vecPaintStart; // used to track where a sweep starts for the purpose of interpolating. |
|
|
|
float m_flFrustration; |
|
|
|
float m_flThinkInterval; |
|
|
|
float m_flDecoyRadius; |
|
|
|
CBeam *m_pBeam; |
|
|
|
bool m_fSnapShot; |
|
|
|
int m_iNumGroupTargets; |
|
CBaseEntity *m_pGroupTarget[ SNIPER_MAX_GROUP_TARGETS ]; |
|
|
|
bool m_bSweepHighestPriority; // My hack :[ (sjb) |
|
int m_iBeamBrightness; |
|
|
|
// bullet stopping energy shield effect. |
|
float m_flShieldDist; |
|
float m_flShieldRadius; |
|
|
|
float m_flTimeLastAttackedPlayer; |
|
|
|
// Protection |
|
EHANDLE m_hProtectTarget; // Entity that this sniper is supposed to protect |
|
float m_flDangerEnemyDistance; // Distance to the enemy nearest the protect target |
|
|
|
// Have I warned the target that I'm pointing my laser at them? |
|
bool m_bWarnedTargetEntity; |
|
|
|
float m_flTimeLastShotMissed; |
|
bool m_bKilledPlayer; |
|
bool m_bShootZombiesInChest; ///< if true, do not try to shoot zombies in the headcrab |
|
|
|
COutputEvent m_OnShotFired; |
|
|
|
DEFINE_CUSTOM_AI; |
|
|
|
DECLARE_DATADESC(); |
|
}; |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
// NOTES about the Sniper: |
|
// |
|
// PATIENCE: |
|
// The concept of "patience" is simply a restriction placed |
|
// on how close a target has to be to the sniper before the |
|
// sniper will take his first shot at the target. This |
|
// distance is referred to as "patience" is set by the ` |
|
// designer in Worldcraft. The sniper won't attack unless |
|
// the target enters this radius. Once the sniper takes |
|
// this first shot, he will not return to a patient state. |
|
// He will then shoot at any/all targets to which there is |
|
// a clear shot, regardless of distance. (sjb) |
|
// |
|
// |
|
// TODO: Sniper accumulates frustration while reloading. |
|
// probably should subtract reload time from frustration. |
|
//========================================================= |
|
//========================================================= |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
short sFlashSprite; |
|
short sHaloSprite; |
|
|
|
//========================================================= |
|
//========================================================= |
|
BEGIN_DATADESC( CProtoSniper ) |
|
|
|
DEFINE_FIELD( m_fWeaponLoaded, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_fEnabled, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_fIsPatient, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_flPatience, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_iMisses, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_hDecoyObject, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_hSweepTarget, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_vecDecoyObjectTarget, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_vecFrustratedTarget, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_vecPaintStart, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_flPaintTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_vecPaintCursor, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_flFrustration, FIELD_TIME ), |
|
DEFINE_FIELD( m_flThinkInterval, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_flDecoyRadius, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_pBeam, FIELD_CLASSPTR ), |
|
DEFINE_FIELD( m_fSnapShot, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_iNumGroupTargets, FIELD_INTEGER ), |
|
DEFINE_ARRAY( m_pGroupTarget, FIELD_CLASSPTR, SNIPER_MAX_GROUP_TARGETS ), |
|
DEFINE_KEYFIELD( m_iBeamBrightness, FIELD_INTEGER, "beambrightness" ), |
|
|
|
|
|
DEFINE_KEYFIELD(m_flShieldDist, FIELD_FLOAT, "shielddistance" ), |
|
DEFINE_KEYFIELD(m_flShieldRadius, FIELD_FLOAT, "shieldradius" ), |
|
DEFINE_KEYFIELD(m_bShootZombiesInChest, FIELD_BOOLEAN, "shootZombiesInChest" ), |
|
|
|
DEFINE_KEYFIELD(m_flKeyfieldPaintTime, FIELD_FLOAT, "PaintInterval" ), |
|
DEFINE_KEYFIELD(m_flKeyfieldPaintTimeNoise, FIELD_FLOAT, "PaintIntervalVariance" ), |
|
|
|
DEFINE_FIELD( m_flTimeLastAttackedPlayer, FIELD_TIME ), |
|
DEFINE_FIELD( m_hProtectTarget, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_flDangerEnemyDistance, FIELD_FLOAT ), |
|
|
|
DEFINE_FIELD( m_bSweepHighestPriority, FIELD_BOOLEAN ), |
|
|
|
DEFINE_FIELD( m_bWarnedTargetEntity, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_flTimeLastShotMissed, FIELD_TIME ), |
|
|
|
// Inputs |
|
DEFINE_INPUTFUNC( FIELD_VOID, "EnableSniper", InputEnableSniper ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "DisableSniper", InputDisableSniper ), |
|
DEFINE_INPUTFUNC( FIELD_INTEGER, "SetDecoyRadius", InputSetDecoyRadius ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "SweepTarget", InputSweepTarget ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "SweepTargetHighestPriority", InputSweepTargetHighestPriority ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "SweepGroupRandomly", InputSweepGroupRandomly ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "StopSweeping", InputStopSweeping ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "ProtectTarget", InputProtectTarget ), |
|
|
|
#if HL2_EPISODIC |
|
DEFINE_INPUTFUNC( FIELD_FLOAT, "SetPaintInterval", InputSetPaintInterval ), |
|
DEFINE_INPUTFUNC( FIELD_FLOAT, "SetPaintIntervalVariance", InputSetPaintIntervalVariance ), |
|
#endif |
|
|
|
// Outputs |
|
DEFINE_OUTPUT( m_OnShotFired, "OnShotFired" ), |
|
|
|
END_DATADESC() |
|
|
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
BEGIN_DATADESC( CSniperBullet ) |
|
|
|
DEFINE_FIELD( m_SoundTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_AmmoType, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_PenetratedAmmoType, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_fActive, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_iImpacts, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_vecOrigin, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_vecDir, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_flLastThink, FIELD_TIME ), |
|
DEFINE_FIELD( m_Speed, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_bDirectShot, FIELD_BOOLEAN ), |
|
|
|
DEFINE_FIELD( m_vecStart, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_vecEnd, FIELD_VECTOR ), |
|
|
|
DEFINE_THINKFUNC( BulletThink ), |
|
|
|
END_DATADESC() |
|
|
|
//========================================================= |
|
// Private conditions |
|
//========================================================= |
|
enum Sniper_Conds |
|
{ |
|
COND_SNIPER_CANATTACKDECOY = LAST_SHARED_CONDITION, |
|
COND_SNIPER_SUPPRESSED, |
|
COND_SNIPER_ENABLED, |
|
COND_SNIPER_DISABLED, |
|
COND_SNIPER_FRUSTRATED, |
|
COND_SNIPER_SWEEP_TARGET, |
|
COND_SNIPER_NO_SHOT, |
|
}; |
|
|
|
|
|
//========================================================= |
|
// schedules |
|
//========================================================= |
|
enum |
|
{ |
|
SCHED_PSNIPER_SCAN = LAST_SHARED_SCHEDULE, |
|
SCHED_PSNIPER_CAMP, |
|
SCHED_PSNIPER_ATTACK, |
|
SCHED_PSNIPER_RELOAD, |
|
SCHED_PSNIPER_ATTACKDECOY, |
|
SCHED_PSNIPER_SUPPRESSED, |
|
SCHED_PSNIPER_DISABLEDWAIT, |
|
SCHED_PSNIPER_FRUSTRATED_ATTACK, |
|
SCHED_PSNIPER_SWEEP_TARGET, |
|
SCHED_PSNIPER_SWEEP_TARGET_NOINTERRUPT, |
|
SCHED_PSNIPER_SNAPATTACK, |
|
SCHED_PSNIPER_NO_CLEAR_SHOT, |
|
SCHED_PSNIPER_PLAYER_DEAD, |
|
}; |
|
|
|
//========================================================= |
|
// tasks |
|
//========================================================= |
|
enum |
|
{ |
|
TASK_SNIPER_FRUSTRATED_ATTACK = LAST_SHARED_TASK, |
|
TASK_SNIPER_PAINT_ENEMY, |
|
TASK_SNIPER_PAINT_DECOY, |
|
TASK_SNIPER_PAINT_FRUSTRATED, |
|
TASK_SNIPER_PAINT_SWEEP_TARGET, |
|
TASK_SNIPER_ATTACK_CURSOR, |
|
TASK_SNIPER_PAINT_NO_SHOT, |
|
TASK_SNIPER_PLAYER_DEAD, |
|
}; |
|
|
|
|
|
|
|
CProtoSniper::CProtoSniper( void ) : m_flKeyfieldPaintTime(SNIPER_DEFAULT_PAINT_ENEMY_TIME), |
|
m_flKeyfieldPaintTimeNoise(SNIPER_DEFAULT_PAINT_NPC_TIME_NOISE) |
|
{ |
|
#ifdef _DEBUG |
|
m_vecPaintCursor.Init(); |
|
m_vecDecoyObjectTarget.Init(); |
|
m_vecFrustratedTarget.Init(); |
|
m_vecPaintStart.Init(); |
|
#endif |
|
|
|
m_iMisses = 0; |
|
m_flDecoyRadius = SNIPER_DECOY_RADIUS; |
|
m_fSnapShot = false; |
|
m_iBeamBrightness = 100; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
bool CProtoSniper::QuerySeeEntity( CBaseEntity *pEntity, bool bOnlyHateOrFearIfNPC ) |
|
{ |
|
Disposition_t disp = IRelationType(pEntity); |
|
if( disp != D_HT ) |
|
{ |
|
// Don't bother with anything I wouldn't shoot. |
|
return false; |
|
} |
|
|
|
if( !FInViewCone(pEntity) ) |
|
{ |
|
// Yes, this does call FInViewCone twice a frame for all entities checked for |
|
// visibility, but doing this allows us to cut out a bunch of traces that would |
|
// be done by VerifyShot for entities that aren't even in our viewcone. |
|
return false; |
|
} |
|
|
|
if( VerifyShot( pEntity ) ) |
|
{ |
|
return BaseClass::QuerySeeEntity(pEntity, bOnlyHateOrFearIfNPC); |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
bool CProtoSniper::FInViewCone ( CBaseEntity *pEntity ) |
|
{ |
|
if( pEntity->GetFlags() & FL_CLIENT ) |
|
{ |
|
CBasePlayer *pPlayer; |
|
|
|
pPlayer = ToBasePlayer( pEntity ); |
|
|
|
if( m_spawnflags & SF_SNIPER_VIEWCONE ) |
|
{ |
|
// See how close this spot is to the laser. |
|
Vector vecEyes; |
|
Vector vecLOS; |
|
float flDist; |
|
Vector vecNearestPoint; |
|
|
|
vecEyes = EyePosition(); |
|
vecLOS = m_vecPaintCursor - vecEyes; |
|
VectorNormalize(vecLOS); |
|
|
|
vecNearestPoint = PointOnLineNearestPoint( EyePosition(), EyePosition() + vecLOS * 8192, pPlayer->EyePosition() ); |
|
|
|
flDist = ( pPlayer->EyePosition() - vecNearestPoint ).Length(); |
|
|
|
if( showsniperdist.GetFloat() != 0 ) |
|
{ |
|
Msg( "Dist from beam: %f\n", flDist ); |
|
} |
|
|
|
if( flDist <= sniperviewdist.GetFloat() ) |
|
{ |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
} |
|
|
|
return BaseClass::FInViewCone( pEntity->EyePosition() ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::LaserOff( void ) |
|
{ |
|
if( m_pBeam ) |
|
{ |
|
UTIL_Remove( m_pBeam); |
|
m_pBeam = NULL; |
|
} |
|
|
|
SetNextThink( gpGlobals->curtime + 0.1f ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
#define LASER_LEAD_DIST 64 |
|
void CProtoSniper::LaserOn( const Vector &vecTarget, const Vector &vecDeviance ) |
|
{ |
|
if (!m_pBeam) |
|
{ |
|
m_pBeam = CBeam::BeamCreate( "effects/bluelaser1.vmt", 1.0f ); |
|
m_pBeam->SetColor( 0, 100, 255 ); |
|
} |
|
else |
|
{ |
|
// Beam seems to be on. |
|
//return; |
|
} |
|
|
|
// Don't aim right at the guy right now. |
|
Vector vecInitialAim; |
|
|
|
if( vecDeviance == vec3_origin ) |
|
{ |
|
// Start the aim where it last left off! |
|
vecInitialAim = m_vecPaintCursor; |
|
} |
|
else |
|
{ |
|
vecInitialAim = vecTarget; |
|
} |
|
|
|
vecInitialAim.x += random->RandomFloat( -vecDeviance.x, vecDeviance.x ); |
|
vecInitialAim.y += random->RandomFloat( -vecDeviance.y, vecDeviance.y ); |
|
vecInitialAim.z += random->RandomFloat( -vecDeviance.z, vecDeviance.z ); |
|
|
|
// The beam is backwards, sortof. The endpoint is the sniper. This is |
|
// so that the beam can be tapered to very thin where it emits from the sniper. |
|
m_pBeam->PointsInit( vecInitialAim, GetBulletOrigin() ); |
|
m_pBeam->SetBrightness( 255 ); |
|
m_pBeam->SetNoise( 0 ); |
|
m_pBeam->SetWidth( 1.0f ); |
|
m_pBeam->SetEndWidth( 0 ); |
|
m_pBeam->SetScrollRate( 0 ); |
|
m_pBeam->SetFadeLength( 0 ); |
|
m_pBeam->SetHaloTexture( sHaloSprite ); |
|
m_pBeam->SetHaloScale( 4.0f ); |
|
|
|
m_vecPaintStart = vecInitialAim; |
|
|
|
// Think faster whilst painting. Higher resolution on the |
|
// beam movement. |
|
SetNextThink( gpGlobals->curtime + 0.02 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Crikey! |
|
//----------------------------------------------------------------------------- |
|
float CProtoSniper::GetPositionParameter( float flTime, bool fLinear ) |
|
{ |
|
float flElapsedTime; |
|
float flTimeParameter; |
|
|
|
flElapsedTime = flTime - (GetWaitFinishTime() - gpGlobals->curtime); |
|
|
|
flTimeParameter = ( flElapsedTime / flTime ); |
|
|
|
if( fLinear ) |
|
{ |
|
return flTimeParameter; |
|
} |
|
else |
|
{ |
|
return (1 + sin( (M_PI * flTimeParameter) - (M_PI / 2) ) ) / 2; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::GetPaintAim( const Vector &vecStart, const Vector &vecGoal, float flParameter, Vector *pProgress ) |
|
{ |
|
#if 0 |
|
Vector vecDelta; |
|
|
|
vecDelta = vecGoal - vecStart; |
|
|
|
float flDist = VectorNormalize( vecDelta ); |
|
|
|
vecDelta = vecStart + vecDelta * (flDist * flParameter); |
|
|
|
vecDelta = (vecDelta - GetBulletOrigin() ).Normalize(); |
|
|
|
*pProgress = vecDelta; |
|
#else |
|
// Quaternions |
|
Vector vecIdealDir; |
|
QAngle vecIdealAngles; |
|
QAngle vecCurrentAngles; |
|
Vector vecCurrentDir; |
|
Vector vecBulletOrigin = GetBulletOrigin(); |
|
|
|
// vecIdealDir is where the gun should be aimed when the painting |
|
// time is up. This can be approximate. This is only for drawing the |
|
// laser, not actually aiming the weapon. A large discrepancy will look |
|
// bad, though. |
|
vecIdealDir = vecGoal - vecBulletOrigin; |
|
VectorNormalize(vecIdealDir); |
|
|
|
// Now turn vecIdealDir into angles! |
|
VectorAngles( vecIdealDir, vecIdealAngles ); |
|
|
|
// This is the vector of the beam's current aim. |
|
vecCurrentDir = vecStart - vecBulletOrigin; |
|
VectorNormalize(vecCurrentDir); |
|
|
|
// Turn this to angles, too. |
|
VectorAngles( vecCurrentDir, vecCurrentAngles ); |
|
|
|
Quaternion idealQuat; |
|
Quaternion currentQuat; |
|
Quaternion aimQuat; |
|
|
|
AngleQuaternion( vecIdealAngles, idealQuat ); |
|
AngleQuaternion( vecCurrentAngles, currentQuat ); |
|
|
|
QuaternionSlerp( currentQuat, idealQuat, flParameter, aimQuat ); |
|
|
|
QuaternionAngles( aimQuat, vecCurrentAngles ); |
|
|
|
// Rebuild the current aim vector. |
|
AngleVectors( vecCurrentAngles, &vecCurrentDir ); |
|
|
|
*pProgress = vecCurrentDir; |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Sweep the laser sight towards the point where the gun should be aimed |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::PaintTarget( const Vector &vecTarget, float flPaintTime ) |
|
{ |
|
Vector vecCurrentDir; |
|
Vector vecStart; |
|
|
|
// vecStart is the barrel of the gun (or the laser sight) |
|
vecStart = GetBulletOrigin(); |
|
|
|
float P; |
|
|
|
// keep painttime from hitting 0 exactly. |
|
flPaintTime = MAX( flPaintTime, 0.000001f ); |
|
|
|
P = GetPositionParameter( flPaintTime, false ); |
|
|
|
// Vital allies are sharper about avoiding the sniper. |
|
if( P > 0.25f && GetEnemy() && GetEnemy()->IsNPC() && HasCondition(COND_SEE_ENEMY) && !m_bWarnedTargetEntity ) |
|
{ |
|
m_bWarnedTargetEntity = true; |
|
|
|
if( GetEnemy()->Classify() == CLASS_PLAYER_ALLY_VITAL && GetEnemy()->MyNPCPointer()->FVisible(this) ) |
|
{ |
|
CSoundEnt::InsertSound( SOUND_DANGER | SOUND_CONTEXT_REACT_TO_SOURCE, GetEnemy()->EarPosition(), 16, 1.0f, this ); |
|
} |
|
} |
|
|
|
GetPaintAim( m_vecPaintStart, vecTarget, clamp(P,0.0f,1.0f), &vecCurrentDir ); |
|
|
|
#if 1 |
|
#define THRESHOLD 0.8f |
|
float flNoiseScale; |
|
|
|
if ( P >= THRESHOLD ) |
|
{ |
|
flNoiseScale = 1 - (1 / (1 - THRESHOLD)) * ( P - THRESHOLD ); |
|
} |
|
else if ( P <= 1 - THRESHOLD ) |
|
{ |
|
flNoiseScale = P / (1 - THRESHOLD); |
|
} |
|
else |
|
{ |
|
flNoiseScale = 1; |
|
} |
|
|
|
// mult by P |
|
vecCurrentDir.x += flNoiseScale * ( sin( 3 * M_PI * gpGlobals->curtime ) * 0.0006 ); |
|
vecCurrentDir.y += flNoiseScale * ( sin( 2 * M_PI * gpGlobals->curtime + 0.5 * M_PI ) * 0.0006 ); |
|
vecCurrentDir.z += flNoiseScale * ( sin( 1.5 * M_PI * gpGlobals->curtime + M_PI ) * 0.0006 ); |
|
#endif |
|
|
|
trace_t tr; |
|
|
|
UTIL_TraceLine( vecStart, vecStart + vecCurrentDir * 8192, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
m_pBeam->SetStartPos( tr.endpos ); |
|
m_pBeam->RelinkBeam(); |
|
|
|
m_vecPaintCursor = tr.endpos; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
bool CProtoSniper::IsPlayerAllySniper() |
|
{ |
|
CBaseEntity *pPlayer = AI_GetSinglePlayer(); |
|
|
|
return IRelationType( pPlayer ) == D_LI; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::InputSetDecoyRadius( inputdata_t &inputdata ) |
|
{ |
|
m_flDecoyRadius = (float)inputdata.value.Int(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::OnScheduleChange( void ) |
|
{ |
|
LaserOff(); |
|
|
|
BaseClass::OnScheduleChange(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
bool CProtoSniper::KeyValue( const char *szKeyName, const char *szValue ) |
|
{ |
|
if (FStrEq(szKeyName, "radius")) |
|
{ |
|
m_flPatience = atof(szValue); |
|
|
|
// If the designer specifies a patience radius of 0, the |
|
// sniper won't have any patience at all. The sniper will |
|
// shoot at the first target it sees regardless of distance. |
|
if( m_flPatience == 0.0 ) |
|
{ |
|
m_fIsPatient = false; |
|
} |
|
else |
|
{ |
|
m_fIsPatient = true; |
|
} |
|
|
|
return true; |
|
} |
|
else if( FStrEq(szKeyName, "misses") ) |
|
{ |
|
m_iMisses = atoi( szValue ); |
|
return true; |
|
} |
|
else |
|
{ |
|
return BaseClass::KeyValue( szKeyName, szValue ); |
|
} |
|
} |
|
|
|
LINK_ENTITY_TO_CLASS( npc_sniper, CProtoSniper ); |
|
LINK_ENTITY_TO_CLASS( proto_sniper, CProtoSniper ); |
|
LINK_ENTITY_TO_CLASS( sniperbullet, CSniperBullet ); |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// |
|
// |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::Precache( void ) |
|
{ |
|
PrecacheModel("models/combine_soldier.mdl"); |
|
sHaloSprite = PrecacheModel("sprites/light_glow03.vmt"); |
|
sFlashSprite = PrecacheModel( "sprites/muzzleflash1.vmt" ); |
|
PrecacheModel("effects/bluelaser1.vmt"); |
|
|
|
UTIL_PrecacheOther( "sniperbullet" ); |
|
|
|
PrecacheScriptSound( "NPC_Sniper.Die" ); |
|
PrecacheScriptSound( "NPC_Sniper.TargetDestroyed" ); |
|
PrecacheScriptSound( "NPC_Sniper.HearDanger"); |
|
PrecacheScriptSound( "NPC_Sniper.FireBullet" ); |
|
PrecacheScriptSound( "NPC_Sniper.Reload" ); |
|
PrecacheScriptSound( "NPC_Sniper.SonicBoom" ); |
|
|
|
BaseClass::Precache(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// |
|
// |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::Spawn( void ) |
|
{ |
|
Precache(); |
|
|
|
/// HACK: |
|
SetModel( "models/combine_soldier.mdl" ); |
|
|
|
//m_hBullet = (CSniperBullet *)Create( "sniperbullet", GetBulletOrigin(), GetLocalAngles(), NULL ); |
|
|
|
//Assert( m_hBullet != NULL ); |
|
|
|
SetHullType( HULL_HUMAN ); |
|
SetHullSizeNormal(); |
|
|
|
UTIL_SetSize( this, Vector( -16, -16 , 0 ), Vector( 16, 16, 64 ) ); |
|
|
|
SetSolid( SOLID_BBOX ); |
|
AddSolidFlags( FSOLID_NOT_STANDABLE ); |
|
SetMoveType( MOVETYPE_FLY ); |
|
m_bloodColor = DONT_BLEED; |
|
m_iHealth = 10; |
|
m_flFieldOfView = 0.2; |
|
m_NPCState = NPC_STATE_NONE; |
|
|
|
if( HasSpawnFlags( SF_SNIPER_STARTDISABLED ) ) |
|
{ |
|
m_fEnabled = false; |
|
} |
|
else |
|
{ |
|
m_fEnabled = true; |
|
} |
|
|
|
CapabilitiesClear(); |
|
CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1 ); |
|
CapabilitiesAdd( bits_CAP_SIMPLE_RADIUS_DAMAGE ); |
|
|
|
m_HackedGunPos = Vector ( 0, 0, 0 ); |
|
|
|
m_spawnflags |= SF_NPC_LONG_RANGE; |
|
m_spawnflags |= SF_NPC_ALWAYSTHINK; |
|
|
|
m_pBeam = NULL; |
|
m_bSweepHighestPriority = false; |
|
|
|
ClearOldDecoys(); |
|
|
|
NPCInit(); |
|
|
|
if( m_spawnflags & SF_SNIPER_HIDDEN ) |
|
{ |
|
AddEffects( EF_NODRAW ); |
|
AddSolidFlags( FSOLID_NOT_SOLID ); |
|
} |
|
|
|
// Point the cursor straight ahead so that the sniper's |
|
// first sweep of the laser doesn't look weird. |
|
Vector vecForward; |
|
AngleVectors( GetLocalAngles(), &vecForward ); |
|
m_vecPaintCursor = GetBulletOrigin() + vecForward * 1024; |
|
|
|
m_fWeaponLoaded = true; |
|
|
|
//m_debugOverlays |= OVERLAY_TEXT_BIT; |
|
|
|
// none! |
|
GetEnemies()->SetFreeKnowledgeDuration( 0.0 ); |
|
|
|
m_flTimeLastAttackedPlayer = 0.0f; |
|
m_bWarnedTargetEntity = false; |
|
m_bKilledPlayer = false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::SetSweepTarget( const char *pszTarget ) |
|
{ |
|
CBaseEntity *pTarget; |
|
|
|
// In case the sniper was sweeping a random set of targets when asked to sweep a normal chain. |
|
ClearTargetGroup(); |
|
|
|
pTarget = gEntList.FindEntityByName( NULL, pszTarget ); |
|
|
|
if( !pTarget ) |
|
{ |
|
DevMsg( "**Sniper %s cannot find sweep target %s\n", GetClassname(), pszTarget ); |
|
m_hSweepTarget = NULL; |
|
return; |
|
} |
|
|
|
m_hSweepTarget = pTarget; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Forces an idle sniper to paint the specified target. |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::InputSweepTarget( inputdata_t &inputdata ) |
|
{ |
|
SetSweepTarget( inputdata.value.String() ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::InputSweepTargetHighestPriority( inputdata_t &inputdata ) |
|
{ |
|
SetSweepTarget( inputdata.value.String() ); |
|
m_bSweepHighestPriority = true; |
|
|
|
if( GetCurSchedule() && stricmp( GetCurSchedule()->GetName(), "SCHED_PSNIPER_RELOAD" ) ) |
|
{ |
|
// If you're doing anything except reloading, stop and do this. |
|
ClearSchedule( "Told to sweep target via input" ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::ClearTargetGroup( void ) |
|
{ |
|
int i; |
|
|
|
for( i = 0 ; i < SNIPER_MAX_GROUP_TARGETS ; i++ ) |
|
{ |
|
m_pGroupTarget[ i ] = NULL; |
|
} |
|
|
|
m_iNumGroupTargets = 0; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Similar to SweepTarget, but forces the sniper to sweep targets |
|
// in a group (bound by groupname) randomly until interrupted. |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::InputSweepGroupRandomly( inputdata_t &inputdata ) |
|
{ |
|
ClearTargetGroup(); |
|
|
|
CBaseEntity *pEnt; |
|
|
|
// PERFORMANCE |
|
// Go through the whole ent list? This could hurt. (sjb) |
|
// Gary: Yes, this sucks. :) |
|
pEnt = gEntList.FirstEnt(); |
|
|
|
do |
|
{ |
|
CSniperTarget *pTarget; |
|
|
|
pTarget = dynamic_cast<CSniperTarget*>(pEnt); |
|
|
|
// If the pointer is null, this isn't a sniper target. |
|
if( pTarget ) |
|
{ |
|
if( !strcmp( inputdata.value.String(), STRING( pTarget->m_iszGroupName ) ) ) |
|
{ |
|
m_pGroupTarget[ m_iNumGroupTargets ] = pTarget; |
|
m_iNumGroupTargets++; |
|
} |
|
} |
|
|
|
pEnt = gEntList.NextEnt( pEnt ); |
|
|
|
} while( pEnt ); |
|
|
|
m_hSweepTarget = m_pGroupTarget[ random->RandomInt( 0, m_iNumGroupTargets - 1 ) ]; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::InputStopSweeping( inputdata_t &inputdata ) |
|
{ |
|
m_hSweepTarget = NULL; |
|
ClearSchedule( "Told to stop sweeping via input" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &inputdata - |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::InputProtectTarget( inputdata_t &inputdata ) |
|
{ |
|
m_hProtectTarget = gEntList.FindEntityByName( NULL, inputdata.value.String(), NULL, inputdata.pActivator, inputdata.pCaller ); |
|
|
|
if ( !m_hProtectTarget ) |
|
{ |
|
DevMsg( "Sniper %s cannot find protect target %s\n", GetClassname(), inputdata.value.String() ); |
|
return; |
|
} |
|
|
|
m_flDangerEnemyDistance = 0; |
|
} |
|
|
|
|
|
|
|
#if HL2_EPISODIC |
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::InputSetPaintInterval( inputdata_t &inputdata ) |
|
{ |
|
m_flKeyfieldPaintTime = inputdata.value.Float(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::InputSetPaintIntervalVariance( inputdata_t &inputdata ) |
|
{ |
|
m_flKeyfieldPaintTimeNoise = inputdata.value.Float(); |
|
} |
|
#endif |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *pTarget - |
|
// Output : int |
|
//----------------------------------------------------------------------------- |
|
int CProtoSniper::IRelationPriority( CBaseEntity *pTarget ) |
|
{ |
|
int priority = BaseClass::IRelationPriority( pTarget ); |
|
|
|
// If we have a target to protect, increase priority on targets closer to it |
|
if ( m_hProtectTarget ) |
|
{ |
|
float flDistance = (pTarget->GetAbsOrigin() - m_hProtectTarget->GetAbsOrigin()).LengthSqr(); |
|
if ( flDistance <= SNIPER_PROTECTION_MINDIST ) |
|
{ |
|
float flBonus = (1.0 - (flDistance / SNIPER_PROTECTION_MINDIST)) * SNIPER_PROTECTION_PRIORITYCAP; |
|
priority += flBonus; |
|
|
|
if ( m_debugOverlays & OVERLAY_NPC_SELECTED_BIT ) |
|
{ |
|
NDebugOverlay::Text( pTarget->GetAbsOrigin() + Vector(0,0,16), UTIL_VarArgs("P: %d (b %f)!", priority, flBonus), false, 0.1 ); |
|
} |
|
} |
|
} |
|
|
|
return priority; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// |
|
// |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
Class_T CProtoSniper::Classify( void ) |
|
{ |
|
if( m_fEnabled ) |
|
{ |
|
return CLASS_PROTOSNIPER; |
|
} |
|
else |
|
{ |
|
return CLASS_NONE; |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
Vector CProtoSniper::GetBulletOrigin( void ) |
|
{ |
|
if( m_spawnflags & SF_SNIPER_HIDDEN ) |
|
{ |
|
return GetAbsOrigin(); |
|
} |
|
else |
|
{ |
|
Vector vecForward; |
|
AngleVectors( GetLocalAngles(), &vecForward ); |
|
return WorldSpaceCenter() + vecForward * 20; |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::ClearOldDecoys( void ) |
|
{ |
|
#if 0 |
|
int i; |
|
|
|
for( i = 0 ; i < NUM_OLDDECOYS ; i++ ) |
|
{ |
|
m_pOldDecoys[ i ] = NULL; |
|
} |
|
|
|
m_iOldDecoySlot = 0; |
|
#endif |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
bool CProtoSniper::HasOldDecoy( CBaseEntity *pDecoy ) |
|
{ |
|
#if 0 |
|
int i; |
|
|
|
for( i = 0 ; i < NUM_OLDDECOYS ; i++ ) |
|
{ |
|
if( m_pOldDecoys[ i ] == pDecoy ) |
|
{ |
|
return true; |
|
} |
|
} |
|
#endif |
|
|
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// The list of old decoys is just a circular list. We put decoys that we've |
|
// already fired at in this list. When they've been pushed off the list by others, |
|
// then they are valid targets again. |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::AddOldDecoy( CBaseEntity *pDecoy ) |
|
{ |
|
#if 0 |
|
m_pOldDecoys[ m_iOldDecoySlot ] = pDecoy; |
|
m_iOldDecoySlot++; |
|
|
|
if( m_iOldDecoySlot == NUM_OLDDECOYS ) |
|
{ |
|
m_iOldDecoySlot = 0; |
|
} |
|
#endif |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Only blast damage can hurt a sniper. |
|
// |
|
// |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
#define SNIPER_MAX_INFLICTOR_DIST 15.0f * 12.0f // 15 feet. |
|
int CProtoSniper::OnTakeDamage_Alive( const CTakeDamageInfo &info ) |
|
{ |
|
if( !m_fEnabled ) |
|
{ |
|
// As good as not existing. |
|
return 0; |
|
} |
|
|
|
if( !info.GetInflictor() ) |
|
return 0; |
|
|
|
CTakeDamageInfo newInfo = info; |
|
|
|
// Allow SetHealth() & npc_kill inputs to hurt the sniper |
|
if ( info.GetDamageType() == DMG_GENERIC && info.GetInflictor() == this ) |
|
return CAI_BaseNPC::OnTakeDamage_Alive( newInfo ); |
|
|
|
if( !(info.GetDamageType() & (DMG_BLAST|DMG_BURN) ) ) |
|
{ |
|
// Only blasts and burning hurt |
|
return 0; |
|
} |
|
|
|
if( (info.GetDamageType() & DMG_BLAST) && info.GetDamage() < m_iHealth ) |
|
{ |
|
// Only blasts powerful enough to kill hurt |
|
return 0; |
|
} |
|
|
|
float flDist = GetAbsOrigin().DistTo( info.GetInflictor()->GetAbsOrigin() ); |
|
if( flDist > SNIPER_MAX_INFLICTOR_DIST ) |
|
{ |
|
// Sniper only takes damage from explosives that are nearby. This makes a sniper |
|
// susceptible to a grenade that lands in his nest, but not to a large explosion |
|
// that goes off elsewhere and just happens to be able to trace into the sniper's |
|
// nest. |
|
return 0; |
|
} |
|
|
|
if( info.GetDamageType() & DMG_BURN ) |
|
{ |
|
newInfo.SetDamage( m_iHealth ); |
|
} |
|
|
|
return CAI_BaseNPC::OnTakeDamage_Alive( newInfo ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: When a sniper is killed, we launch a fake ragdoll corpse as if the |
|
// sniper was blasted out of his nest. |
|
// |
|
// |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::Event_Killed( const CTakeDamageInfo &info ) |
|
{ |
|
if( !(m_spawnflags & SF_SNIPER_NOCORPSE) ) |
|
{ |
|
Vector vecForward; |
|
|
|
float flForce = random->RandomFloat( 500, 700 ) * 10; |
|
|
|
AngleVectors( GetLocalAngles(), &vecForward ); |
|
|
|
float flFadeTime = 0.0; |
|
|
|
if( HasSpawnFlags( SF_NPC_FADE_CORPSE ) ) |
|
{ |
|
flFadeTime = 5.0; |
|
} |
|
|
|
CBaseEntity *pGib; |
|
bool bShouldIgnite = IsOnFire() || hl2_episodic.GetBool(); |
|
pGib = CreateRagGib( "models/combine_soldier.mdl", GetLocalOrigin(), GetLocalAngles(), (vecForward * flForce) + Vector(0, 0, 600), flFadeTime, bShouldIgnite ); |
|
|
|
} |
|
|
|
m_OnDeath.FireOutput( info.GetAttacker(), this ); |
|
|
|
// Tell my killer that he got me! |
|
if( info.GetAttacker() ) |
|
{ |
|
info.GetAttacker()->Event_KilledOther(this, info); |
|
g_EventQueue.AddEvent( info.GetAttacker(), "KilledNPC", 0.3, this, this ); |
|
} |
|
|
|
LaserOff(); |
|
|
|
EmitSound( "NPC_Sniper.Die" ); |
|
|
|
UTIL_Remove( this ); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CProtoSniper::Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ) |
|
{ |
|
if( pVictim && pVictim->IsPlayer() ) |
|
{ |
|
m_bKilledPlayer = true; |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CProtoSniper::UpdateOnRemove( void ) |
|
{ |
|
LaserOff(); |
|
BaseClass::UpdateOnRemove(); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
int CProtoSniper::SelectSchedule ( void ) |
|
{ |
|
if( HasCondition(COND_ENEMY_DEAD) && sniperspeak.GetBool() ) |
|
{ |
|
EmitSound( "NPC_Sniper.TargetDestroyed" ); |
|
} |
|
|
|
if( !m_fWeaponLoaded ) |
|
{ |
|
// Reload is absolute priority. |
|
return SCHED_RELOAD; |
|
} |
|
|
|
if( !AI_GetSinglePlayer()->IsAlive() && m_bKilledPlayer ) |
|
{ |
|
if( HasCondition(COND_IN_PVS) ) |
|
{ |
|
return SCHED_PSNIPER_PLAYER_DEAD; |
|
} |
|
} |
|
|
|
if( HasCondition( COND_HEAR_DANGER ) ) |
|
{ |
|
// Next priority is to be suppressed! |
|
ScopeGlint(); |
|
|
|
CSound *pSound = GetBestSound(); |
|
|
|
if( pSound && pSound->IsSoundType( SOUND_DANGER ) && BaseClass::FVisible( pSound->GetSoundReactOrigin() ) ) |
|
{ |
|
// The sniper will scream if the sound of a grenade about to detonate is heard. |
|
// If this COND_HEAR_DANGER is due to the sound really being SOUND_DANGER_SNIPERONLY, |
|
// the sniper keeps quiet, because the player's grenade might miss the mark. |
|
|
|
// Make sure the sound is visible, otherwise the sniper will scream at a grenade that |
|
// probably won't harm him. |
|
|
|
// Also, don't play the sound effect if we're an ally. |
|
if ( IsPlayerAllySniper() == false ) |
|
{ |
|
EmitSound( "NPC_Sniper.HearDanger" ); |
|
} |
|
} |
|
|
|
return SCHED_PSNIPER_SUPPRESSED; |
|
} |
|
|
|
// OK. If you fall through all the cases above, but you're DISABLED, |
|
// play the schedule that waits a little while and tries again. |
|
if( !m_fEnabled ) |
|
{ |
|
return SCHED_PSNIPER_DISABLEDWAIT; |
|
} |
|
|
|
if( HasCondition( COND_SNIPER_SWEEP_TARGET ) ) |
|
{ |
|
// Sweep a target. Scripted by level designers! |
|
if( ( m_hSweepTarget && m_hSweepTarget->HasSpawnFlags( SF_SNIPERTARGET_NOINTERRUPT ) ) || m_bSweepHighestPriority ) |
|
{ |
|
return SCHED_PSNIPER_SWEEP_TARGET_NOINTERRUPT; |
|
} |
|
else |
|
{ |
|
return SCHED_PSNIPER_SWEEP_TARGET; |
|
} |
|
} |
|
|
|
if( GetEnemy() == NULL || HasCondition( COND_ENEMY_DEAD ) ) |
|
{ |
|
// Look for an enemy. |
|
SetEnemy( NULL ); |
|
return SCHED_PSNIPER_SCAN; |
|
} |
|
|
|
if( HasCondition( COND_SNIPER_FRUSTRATED ) ) |
|
{ |
|
return SCHED_PSNIPER_FRUSTRATED_ATTACK; |
|
} |
|
|
|
if( HasCondition( COND_SNIPER_CANATTACKDECOY ) ) |
|
{ |
|
return SCHED_RANGE_ATTACK2; |
|
} |
|
|
|
if( HasCondition( COND_SNIPER_NO_SHOT ) ) |
|
{ |
|
return SCHED_PSNIPER_NO_CLEAR_SHOT; |
|
} |
|
|
|
if( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) |
|
{ |
|
// shoot! |
|
return SCHED_RANGE_ATTACK1; |
|
} |
|
else |
|
{ |
|
// Camp on this target |
|
return SCHED_PSNIPER_CAMP; |
|
} |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
int CProtoSniper::GetSoundInterests( void ) |
|
{ |
|
// Suppress when you hear danger sound |
|
if( m_fEnabled ) |
|
{ |
|
return SOUND_DANGER | SOUND_DANGER_SNIPERONLY; |
|
} |
|
|
|
return SOUND_NONE; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CProtoSniper::OnListened() |
|
{ |
|
BaseClass::OnListened(); |
|
|
|
AISoundIter_t iter; |
|
Vector forward; |
|
|
|
GetVectors( &forward, NULL, NULL ); |
|
|
|
CSound *pCurrentSound = GetSenses()->GetFirstHeardSound( &iter ); |
|
while ( pCurrentSound ) |
|
{ |
|
// the npc cares about this sound, and it's close enough to hear. |
|
if ( pCurrentSound->FIsSound() ) |
|
{ |
|
// this is an audible sound. |
|
if( pCurrentSound->SoundTypeNoContext() == SOUND_DANGER_SNIPERONLY ) |
|
{ |
|
SetCondition( COND_HEAR_DANGER ); |
|
} |
|
#if 0 |
|
if( pCurrentSound->IsSoundType( SOUND_BULLET_IMPACT ) ) |
|
{ |
|
// Clip this bullet to the shield. |
|
if( pCurrentSound->m_hOwner ) |
|
{ |
|
Ray_t ray; |
|
cplane_t plane; |
|
|
|
ray.Init( pCurrentSound->m_hOwner->EyePosition(), pCurrentSound->GetSoundOrigin(), Vector( 0, 0, 0 ), Vector( 0, 0, 0 ) ); |
|
|
|
plane.normal = forward; |
|
plane.type = PLANE_ANYX; |
|
plane.dist = DotProduct( plane.normal, WorldSpaceCenter() + forward * m_flShieldDist ); |
|
plane.signbits = SignbitsForPlane(&plane); |
|
|
|
float fraction = IntersectRayWithPlane( ray, plane ); |
|
|
|
Vector vecImpactPoint = ray.m_Start + ray.m_Delta * fraction; |
|
|
|
float flDist = (vecImpactPoint - (WorldSpaceCenter() + forward * m_flShieldDist)).LengthSqr(); |
|
|
|
if( flDist <= (m_flShieldRadius * m_flShieldRadius) ) |
|
{ |
|
CEffectData data; |
|
|
|
data.m_vOrigin = vecImpactPoint; |
|
data.m_vNormal = vec3_origin; |
|
data.m_vAngles = vec3_angle; |
|
data.m_nColor = COMMAND_POINT_YELLOW; |
|
|
|
DispatchEffect( "CommandPointer", data ); |
|
} |
|
} |
|
} |
|
#endif |
|
} |
|
|
|
pCurrentSound = GetSenses()->GetNextHeardSound( &iter ); |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
bool CProtoSniper::FCanCheckAttacks ( void ) |
|
{ |
|
return true; |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
bool CProtoSniper::FindDecoyObject( void ) |
|
{ |
|
#define SEARCH_DEPTH 50 |
|
|
|
CBaseEntity *pDecoys[ SNIPER_NUM_DECOYS ]; |
|
CBaseEntity *pList[ SEARCH_DEPTH ]; |
|
CBaseEntity *pCurrent; |
|
int count; |
|
int i; |
|
Vector vecTarget = GetEnemy()->WorldSpaceCenter(); |
|
Vector vecDelta; |
|
|
|
m_hDecoyObject = NULL; |
|
|
|
for( i = 0 ; i < SNIPER_NUM_DECOYS ; i++ ) |
|
{ |
|
pDecoys[ i ] = NULL; |
|
} |
|
|
|
vecDelta.x = m_flDecoyRadius; |
|
vecDelta.y = m_flDecoyRadius; |
|
vecDelta.z = m_flDecoyRadius; |
|
|
|
count = UTIL_EntitiesInBox( pList, SEARCH_DEPTH, vecTarget - vecDelta, vecTarget + vecDelta, 0 ); |
|
|
|
// Now we have the list of entities near the target. |
|
// Dig through that list and build the list of decoys. |
|
int iIterator = 0; |
|
|
|
for( i = 0 ; i < count ; i++ ) |
|
{ |
|
pCurrent = pList[ i ]; |
|
|
|
if( FClassnameIs( pCurrent, "func_breakable" ) || FClassnameIs( pCurrent, "prop_physics" ) || FClassnameIs( pCurrent, "func_physbox" ) ) |
|
{ |
|
if( !pCurrent->VPhysicsGetObject() ) |
|
continue; |
|
|
|
if( pCurrent->VPhysicsGetObject()->GetMass() > SNIPER_DECOY_MAX_MASS ) |
|
{ |
|
// Skip this very heavy object. Probably a car or dumpster. |
|
continue; |
|
} |
|
|
|
if( pCurrent->VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) |
|
{ |
|
// Ah! If the player is holding something, try to shoot it! |
|
if( FVisible( pCurrent ) ) |
|
{ |
|
m_hDecoyObject = pCurrent; |
|
m_vecDecoyObjectTarget = pCurrent->WorldSpaceCenter(); |
|
return true; |
|
} |
|
} |
|
|
|
// This item meets criteria for a decoy object to shoot at. |
|
|
|
// But have we shot at this item recently? If we HAVE, don't add it. |
|
#if 0 |
|
if( !HasOldDecoy( pCurrent ) ) |
|
#endif |
|
{ |
|
pDecoys[ iIterator ] = pCurrent; |
|
|
|
if( iIterator == SNIPER_NUM_DECOYS - 1 ) |
|
{ |
|
break; |
|
} |
|
else |
|
{ |
|
iIterator++; |
|
} |
|
} |
|
} |
|
} |
|
|
|
if( iIterator == 0 ) |
|
{ |
|
return false; |
|
} |
|
|
|
// try 4 times to pick a random object from the list |
|
// and trace to it. If the trace goes off, that's the object! |
|
|
|
for( i = 0 ; i < 4 ; i++ ) |
|
{ |
|
CBaseEntity *pProspect; |
|
trace_t tr; |
|
|
|
// Pick one of the decoys at random. |
|
pProspect = pDecoys[ random->RandomInt( 0, iIterator - 1 ) ]; |
|
|
|
Vector vecDecoyTarget; |
|
Vector vecDirToDecoy; |
|
Vector vecBulletOrigin; |
|
|
|
vecBulletOrigin = GetBulletOrigin(); |
|
pProspect->CollisionProp()->RandomPointInBounds( Vector( .1, .1, .1 ), Vector( .6, .6, .6 ), &vecDecoyTarget ); |
|
|
|
// When trying to trace to an object using its absmin + some fraction of its size, it's best |
|
// to lengthen the trace a little beyond the object's bounding box in case it's a more complex |
|
// object, or not axially aligned. |
|
vecDirToDecoy = vecDecoyTarget - vecBulletOrigin; |
|
VectorNormalize(vecDirToDecoy); |
|
|
|
|
|
// Right now, tracing with MASK_BLOCKLOS and checking the fraction as well as the object the trace |
|
// has hit makes it possible for the decoy behavior to shoot through glass. |
|
UTIL_TraceLine( vecBulletOrigin, vecDecoyTarget + vecDirToDecoy * 32, |
|
MASK_BLOCKLOS, this, COLLISION_GROUP_NONE, &tr); |
|
|
|
if( tr.m_pEnt == pProspect || tr.fraction == 1.0 ) |
|
{ |
|
// Great! A shot will hit this object. |
|
m_hDecoyObject = pProspect; |
|
m_vecDecoyObjectTarget = tr.endpos; |
|
|
|
// Throw some noise in, don't always hit the center. |
|
Vector vecNoise; |
|
pProspect->CollisionProp()->RandomPointInBounds( Vector( 0.25, 0.25, 0.25 ), Vector( 0.75, 0.75, 0.75 ), &vecNoise ); |
|
m_vecDecoyObjectTarget += vecNoise - pProspect->GetAbsOrigin(); |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
#define SNIPER_SNAP_SHOT_VELOCITY 125 |
|
bool CProtoSniper::ShouldSnapShot( void ) |
|
{ |
|
if( GetEnemy()->IsPlayer() ) |
|
{ |
|
if( GetEnemy()->GetSmoothedVelocity().Length() >= SNIPER_SNAP_SHOT_VELOCITY ) |
|
{ |
|
return true; |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
} |
|
|
|
// Right now, always snapshot at NPC's |
|
return true; |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
bool CProtoSniper::VerifyShot( CBaseEntity *pTarget ) |
|
{ |
|
trace_t tr; |
|
|
|
Vector vecTarget = DesiredBodyTarget( pTarget ); |
|
UTIL_TraceLine( GetBulletOrigin(), vecTarget, MASK_SHOT, pTarget, COLLISION_GROUP_NONE, &tr ); |
|
|
|
if( tr.fraction != 1.0 ) |
|
{ |
|
if( pTarget->IsPlayer() ) |
|
{ |
|
// if the target is the player, do another trace to see if we can shoot his eyeposition. This should help |
|
// improve sniper responsiveness in cases where the player is hiding his chest from the sniper with his |
|
// head in full view. |
|
UTIL_TraceLine( GetBulletOrigin(), pTarget->EyePosition(), MASK_SHOT, pTarget, COLLISION_GROUP_NONE, &tr ); |
|
|
|
if( tr.fraction == 1.0 ) |
|
{ |
|
return true; |
|
} |
|
} |
|
|
|
// Trace hit something. |
|
if( tr.m_pEnt ) |
|
{ |
|
if( tr.m_pEnt->m_takedamage == DAMAGE_YES ) |
|
{ |
|
// Just shoot it if I can hurt it. Probably a breakable or glass pane. |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
else |
|
{ |
|
return true; |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
int CProtoSniper::RangeAttack1Conditions ( float flDot, float flDist ) |
|
{ |
|
float fFrustration; |
|
fFrustration = gpGlobals->curtime - m_flFrustration; |
|
|
|
//Msg( "Frustration: %f\n", fFrustration ); |
|
|
|
if( HasCondition( COND_SEE_ENEMY ) && !HasCondition( COND_ENEMY_OCCLUDED ) ) |
|
{ |
|
if( VerifyShot( GetEnemy() ) ) |
|
{ |
|
// Can see the enemy, have a clear shot to his midsection |
|
ClearCondition( COND_SNIPER_NO_SHOT ); |
|
} |
|
else |
|
{ |
|
// Can see the enemy, but can't take a shot at his midsection |
|
SetCondition( COND_SNIPER_NO_SHOT ); |
|
return COND_NONE; |
|
} |
|
|
|
if( m_fIsPatient ) |
|
{ |
|
// This sniper has a clear shot at the target, but can not take |
|
// the shot if he is being patient and the target is outside |
|
// of the patience radius. |
|
|
|
float flDist; |
|
|
|
flDist = ( GetLocalOrigin() - GetEnemy()->GetLocalOrigin() ).Length2D(); |
|
|
|
if( flDist <= m_flPatience ) |
|
{ |
|
// This target is close enough to attack! |
|
return COND_CAN_RANGE_ATTACK1; |
|
} |
|
else |
|
{ |
|
// Be patient... |
|
return COND_NONE; |
|
} |
|
} |
|
else |
|
{ |
|
// Not being patient. Clear for attack. |
|
return COND_CAN_RANGE_ATTACK1; |
|
} |
|
} |
|
|
|
if( fFrustration >= 2 && !m_fIsPatient ) |
|
{ |
|
if( !(m_spawnflags & SF_SNIPER_NOSWEEP) && !m_hDecoyObject && FindDecoyObject() ) |
|
{ |
|
// If I don't have a decoy, try to find one and shoot it. |
|
return COND_SNIPER_CANATTACKDECOY; |
|
} |
|
|
|
|
|
if( fFrustration >= 2.5 ) |
|
{ |
|
// Otherwise, just fire somewhere near the hiding enemy. |
|
return COND_SNIPER_FRUSTRATED; |
|
} |
|
} |
|
|
|
return COND_NONE; |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
int CProtoSniper::TranslateSchedule( int scheduleType ) |
|
{ |
|
switch( scheduleType ) |
|
{ |
|
case SCHED_RANGE_ATTACK1: |
|
if( m_hSweepTarget != NULL && m_fSnapShot && ShouldSnapShot() ) |
|
{ |
|
return SCHED_PSNIPER_SNAPATTACK; |
|
} |
|
|
|
return SCHED_PSNIPER_ATTACK; |
|
break; |
|
|
|
case SCHED_RANGE_ATTACK2: |
|
return SCHED_PSNIPER_ATTACKDECOY; |
|
break; |
|
|
|
case SCHED_RELOAD: |
|
return SCHED_PSNIPER_RELOAD; |
|
break; |
|
} |
|
return BaseClass::TranslateSchedule( scheduleType ); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CProtoSniper::ScopeGlint() |
|
{ |
|
CEffectData data; |
|
|
|
data.m_vOrigin = GetAbsOrigin(); |
|
data.m_vNormal = vec3_origin; |
|
data.m_vAngles = vec3_angle; |
|
data.m_nColor = COMMAND_POINT_BLUE; |
|
|
|
DispatchEffect( "CommandPointer", data ); |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
// This starts the bullet state machine. The actual effects |
|
// of the bullet will happen later. This function schedules |
|
// those effects. |
|
// |
|
// fDirectShot indicates whether the bullet is a "direct shot" |
|
// that is - fired with the intent that it will strike the |
|
// enemy. Otherwise, the bullet is intended to strike a |
|
// decoy object or nothing at all in particular. |
|
//--------------------------------------------------------- |
|
bool CProtoSniper::FireBullet( const Vector &vecTarget, bool bDirectShot ) |
|
{ |
|
CSniperBullet *pBullet; |
|
Vector vecBulletOrigin; |
|
|
|
vecBulletOrigin = GetBulletOrigin(); |
|
|
|
pBullet = (CSniperBullet *)Create( "sniperbullet", GetBulletOrigin(), GetLocalAngles(), NULL ); |
|
|
|
Assert( pBullet != NULL ); |
|
|
|
if( !pBullet->Start( vecBulletOrigin, vecTarget, this, bDirectShot ) ) |
|
{ |
|
// Bullet must still be active. |
|
return false; |
|
} |
|
|
|
pBullet->SetOwnerEntity( this ); |
|
|
|
CPASAttenuationFilter filternoatten( this, ATTN_NONE ); |
|
EmitSound( filternoatten, entindex(), "NPC_Sniper.FireBullet" ); |
|
|
|
CPVSFilter filter( vecBulletOrigin ); |
|
te->Sprite( filter, 0.0, &vecBulletOrigin, sFlashSprite, 0.3, 255 ); |
|
|
|
// force a reload when we're done |
|
m_fWeaponLoaded = false; |
|
|
|
// Once the sniper takes a shot, turn the patience off! |
|
m_fIsPatient = false; |
|
|
|
// Alleviate frustration, too! |
|
m_flFrustration = gpGlobals->curtime; |
|
|
|
// This may have been a snap shot. |
|
// Don't allow subsequent snap shots. |
|
m_fSnapShot = false; |
|
|
|
// Return to normal priority |
|
m_bSweepHighestPriority = false; |
|
|
|
// Sniper had to be aiming here to fire here. |
|
// Make it the cursor. |
|
m_vecPaintCursor = vecTarget; |
|
|
|
m_hDecoyObject.Set( NULL ); |
|
|
|
m_OnShotFired.FireOutput( GetEnemy(), this ); |
|
|
|
return true; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
float CProtoSniper::GetBulletSpeed() |
|
{ |
|
float speed = bulletSpeed.GetFloat(); |
|
|
|
if( IsFastSniper() ) |
|
{ |
|
speed *= 2.5f; |
|
} |
|
|
|
return speed; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CProtoSniper::StartTask( const Task_t *pTask ) |
|
{ |
|
switch( pTask->iTask ) |
|
{ |
|
case TASK_SNIPER_PLAYER_DEAD: |
|
{ |
|
m_hSweepTarget = AI_GetSinglePlayer(); |
|
SetWait( 4.0f ); |
|
LaserOn( m_hSweepTarget->GetAbsOrigin(), vec3_origin ); |
|
} |
|
break; |
|
|
|
case TASK_SNIPER_ATTACK_CURSOR: |
|
break; |
|
|
|
case TASK_RANGE_ATTACK1: |
|
// Start task does nothing here. |
|
// We fall through to RunTask() which will keep trying to take |
|
// the shot until the weapon is ready to fire. In some rare cases, |
|
// the weapon may be ready to fire before the single bullet allocated |
|
// to the sniper has hit its target. |
|
break; |
|
|
|
case TASK_RANGE_ATTACK2: |
|
// Don't call up to base class, it will try to set the activity. |
|
break; |
|
|
|
case TASK_SNIPER_PAINT_SWEEP_TARGET: |
|
if ( !m_hSweepTarget.Get() ) |
|
{ |
|
TaskFail( FAIL_NO_TARGET ); |
|
return; |
|
} |
|
|
|
SetWait( m_hSweepTarget->m_flSpeed ); |
|
|
|
// Snap directly to this target if this spawnflag is set. |
|
// Otherwise, sweep from wherever the cursor was. |
|
if( m_hSweepTarget->HasSpawnFlags( SF_SNIPERTARGET_SNAPTO ) ) |
|
{ |
|
m_vecPaintCursor = m_hSweepTarget->GetLocalOrigin(); |
|
} |
|
|
|
LaserOn( m_hSweepTarget->GetLocalOrigin(), vec3_origin ); |
|
break; |
|
|
|
case TASK_SNIPER_PAINT_ENEMY: |
|
// Everytime we start to paint an enemy, this is reset to false. |
|
m_bWarnedTargetEntity = false; |
|
|
|
// If the sniper has a sweep target, clear it, unless it's flagged to resume |
|
if( m_hSweepTarget != NULL ) |
|
{ |
|
if ( !m_hSweepTarget->HasSpawnFlags( SF_SNIPERTARGET_RESUME) ) |
|
{ |
|
ClearTargetGroup(); |
|
m_hSweepTarget = NULL; |
|
} |
|
} |
|
|
|
if( m_spawnflags & SF_SNIPER_VIEWCONE ) |
|
{ |
|
SetWait( SNIPER_FOG_PAINT_ENEMY_TIME ); |
|
|
|
// Just turn it on where it is. |
|
LaserOn( m_vecPaintCursor, vec3_origin ); |
|
} |
|
else |
|
{ |
|
if( GetEnemy()->IsPlayer() ) |
|
{ |
|
float delay = 0; |
|
#ifdef _XBOX |
|
delay += sniper_xbox_delay.GetFloat(); |
|
#endif |
|
|
|
if( gpGlobals->curtime - m_flTimeLastAttackedPlayer <= SNIPER_FASTER_ATTACK_PERIOD ) |
|
{ |
|
SetWait( SNIPER_SUBSEQUENT_PAINT_TIME + delay ); |
|
m_flPaintTime = SNIPER_SUBSEQUENT_PAINT_TIME + delay; |
|
} |
|
else |
|
{ |
|
SetWait( m_flKeyfieldPaintTime + delay ); |
|
m_flPaintTime = m_flKeyfieldPaintTime + delay; |
|
} |
|
} |
|
else |
|
{ |
|
m_flPaintTime = m_flKeyfieldPaintTimeNoise > 0 ? |
|
m_flKeyfieldPaintTime + random->RandomFloat( 0, m_flKeyfieldPaintTimeNoise ) : |
|
m_flKeyfieldPaintTime |
|
; |
|
|
|
if( IsFastSniper() ) |
|
{ |
|
// Get the shot off a little faster. |
|
m_flPaintTime *= 0.75f; |
|
} |
|
|
|
SetWait( m_flPaintTime ); |
|
} |
|
|
|
Vector vecCursor; |
|
|
|
if ( m_spawnflags & SF_SNIPER_NOSWEEP ) |
|
{ |
|
LaserOn( m_vecPaintCursor, vec3_origin ); |
|
} |
|
else |
|
{ |
|
// Try to start the laser where the player can't miss seeing it! |
|
AngleVectors( GetEnemy()->GetLocalAngles(), &vecCursor ); |
|
vecCursor = vecCursor * 300; |
|
vecCursor += GetEnemy()->EyePosition(); |
|
LaserOn( vecCursor, Vector( 16, 16, 16 ) ); |
|
} |
|
|
|
} |
|
|
|
// Scope glints if shooting at player. |
|
if( GetEnemy()->IsPlayer() ) |
|
{ |
|
ScopeGlint(); |
|
} |
|
|
|
break; |
|
|
|
case TASK_SNIPER_PAINT_NO_SHOT: |
|
SetWait( SNIPER_PAINT_NO_SHOT_TIME ); |
|
if( FindFrustratedShot( pTask->flTaskData ) ) |
|
{ |
|
LaserOff(); |
|
LaserOn( m_vecFrustratedTarget, vec3_origin ); |
|
} |
|
else |
|
{ |
|
TaskFail( "Frustrated shot with no enemy" ); |
|
} |
|
break; |
|
|
|
case TASK_SNIPER_PAINT_FRUSTRATED: |
|
m_flPaintTime = SNIPER_PAINT_FRUSTRATED_TIME + random->RandomFloat( 0, SNIPER_PAINT_FRUSTRATED_TIME ); |
|
SetWait( m_flPaintTime ); |
|
if( FindFrustratedShot( pTask->flTaskData ) ) |
|
{ |
|
LaserOff(); |
|
LaserOn( m_vecFrustratedTarget, vec3_origin ); |
|
} |
|
else |
|
{ |
|
TaskFail( "Frustrated shot with no enemy" ); |
|
} |
|
break; |
|
|
|
case TASK_SNIPER_PAINT_DECOY: |
|
SetWait( pTask->flTaskData ); |
|
LaserOn( m_vecDecoyObjectTarget, Vector( 64, 64, 64 ) ); |
|
break; |
|
|
|
case TASK_RELOAD: |
|
{ |
|
CPASAttenuationFilter filter( this ); |
|
EmitSound( filter, entindex(), "NPC_Sniper.Reload" ); |
|
m_fWeaponLoaded = true; |
|
TaskComplete(); |
|
} |
|
break; |
|
|
|
case TASK_SNIPER_FRUSTRATED_ATTACK: |
|
//FindFrustratedShot(); |
|
break; |
|
|
|
default: |
|
BaseClass::StartTask( pTask ); |
|
break; |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CProtoSniper::RunTask( const Task_t *pTask ) |
|
{ |
|
switch( pTask->iTask ) |
|
{ |
|
case TASK_SNIPER_PLAYER_DEAD: |
|
if( IsWaitFinished() ) |
|
{ |
|
m_hSweepTarget = PickDeadPlayerTarget(); |
|
m_vecPaintStart = m_vecPaintCursor; |
|
SetWait( 4.0f ); |
|
} |
|
else |
|
{ |
|
PaintTarget( m_hSweepTarget->GetAbsOrigin(), 4.0f ); |
|
} |
|
break; |
|
|
|
case TASK_SNIPER_ATTACK_CURSOR: |
|
if( FireBullet( m_vecPaintCursor, true ) ) |
|
{ |
|
TaskComplete(); |
|
} |
|
break; |
|
|
|
case TASK_RANGE_ATTACK1: |
|
// Fire at enemy. |
|
if( FireBullet( LeadTarget( GetEnemy() ), true ) ) |
|
{ |
|
// Msg("Firing at %s\n",GetEnemy()->GetEntityName().ToCStr()); |
|
|
|
if( GetEnemy() && GetEnemy()->IsPlayer() ) |
|
{ |
|
m_flTimeLastAttackedPlayer = gpGlobals->curtime; |
|
} |
|
|
|
TaskComplete(); |
|
} |
|
else |
|
{ |
|
// Msg("Firebullet %s is false\n",GetEnemy()->GetEntityName().ToCStr()); |
|
} |
|
break; |
|
|
|
case TASK_SNIPER_FRUSTRATED_ATTACK: |
|
if( FireBullet( m_vecFrustratedTarget, false ) ) |
|
{ |
|
TaskComplete(); |
|
} |
|
break; |
|
|
|
case TASK_SNIPER_PAINT_SWEEP_TARGET: |
|
if ( !m_hSweepTarget.Get() ) |
|
{ |
|
TaskFail( FAIL_NO_TARGET ); |
|
return; |
|
} |
|
|
|
if( IsWaitFinished() ) |
|
{ |
|
// Time up! Paint the next target in the chain, or stop. |
|
CBaseEntity *pNext; |
|
pNext = gEntList.FindEntityByName( NULL, m_hSweepTarget->m_target ); |
|
|
|
if ( m_hSweepTarget->HasSpawnFlags( SF_SNIPERTARGET_SHOOTME ) ) |
|
{ |
|
FireBullet( m_hSweepTarget->GetLocalOrigin(), false ); |
|
TaskComplete(); // Force a reload. |
|
} |
|
|
|
if( pNext || IsSweepingRandomly() ) |
|
{ |
|
// Bump the timer up, update the cursor, paint the new target! |
|
// This is done regardless of whether we just fired at the current target. |
|
|
|
m_vecPaintCursor = m_hSweepTarget->GetLocalOrigin(); |
|
if( IsSweepingRandomly() ) |
|
{ |
|
// If sweeping randomly, just pick another target. |
|
CBaseEntity *pOldTarget; |
|
|
|
pOldTarget = m_hSweepTarget; |
|
|
|
// Pick another target in the group. Don't shoot at the one we just shot at. |
|
if( m_iNumGroupTargets > 1 ) |
|
{ |
|
do |
|
{ |
|
m_hSweepTarget = m_pGroupTarget[ random->RandomInt( 0, m_iNumGroupTargets - 1 ) ]; |
|
} while( m_hSweepTarget == pOldTarget ); |
|
} |
|
} |
|
else |
|
{ |
|
// If not, go with the next target in the chain. |
|
m_hSweepTarget = pNext; |
|
} |
|
|
|
m_vecPaintStart = m_vecPaintCursor; |
|
SetWait( m_hSweepTarget->m_flSpeed ); |
|
} |
|
else |
|
{ |
|
m_hSweepTarget = NULL; |
|
LaserOff(); |
|
TaskComplete(); |
|
} |
|
|
|
#if 0 |
|
NDebugOverlay::Line(GetBulletOrigin(), m_hSweepTarget->GetLocalOrigin(), 0,255,0, true, 20 ); |
|
#endif |
|
} |
|
else |
|
{ |
|
if ( m_hSweepTarget->HasSpawnFlags( SF_SNIPERTARGET_SNAPSHOT ) ) |
|
{ |
|
m_fSnapShot = true; |
|
} |
|
|
|
PaintTarget( m_hSweepTarget->GetAbsOrigin(), m_hSweepTarget->m_flSpeed ); |
|
} |
|
|
|
break; |
|
|
|
case TASK_SNIPER_PAINT_ENEMY: |
|
if( IsWaitFinished() ) |
|
{ |
|
TaskComplete(); |
|
} |
|
|
|
PaintTarget( LeadTarget( GetEnemy() ), m_flPaintTime ); |
|
break; |
|
|
|
case TASK_SNIPER_PAINT_DECOY: |
|
if( IsWaitFinished() ) |
|
{ |
|
TaskComplete(); |
|
} |
|
|
|
PaintTarget( m_vecDecoyObjectTarget, pTask->flTaskData ); |
|
break; |
|
|
|
case TASK_SNIPER_PAINT_NO_SHOT: |
|
if( IsWaitFinished() ) |
|
{ |
|
//HACKHACK(sjb) |
|
// This condition should be turned off |
|
// by a task. |
|
ClearCondition( COND_SNIPER_NO_SHOT ); |
|
TaskComplete(); |
|
} |
|
|
|
PaintTarget( m_vecFrustratedTarget, SNIPER_PAINT_NO_SHOT_TIME ); |
|
break; |
|
|
|
case TASK_SNIPER_PAINT_FRUSTRATED: |
|
if( IsWaitFinished() ) |
|
{ |
|
TaskComplete(); |
|
} |
|
|
|
PaintTarget( m_vecFrustratedTarget, m_flPaintTime ); |
|
break; |
|
|
|
case TASK_RANGE_ATTACK2: |
|
// Fire at decoy |
|
if( m_hDecoyObject == NULL ) |
|
{ |
|
TaskFail("sniper: bad decoy"); |
|
break; |
|
} |
|
|
|
if( FireBullet( m_vecDecoyObjectTarget, false ) ) |
|
{ |
|
//Msg( "Fired at decoy\n" ); |
|
AddOldDecoy( m_hDecoyObject ); |
|
TaskComplete(); |
|
} |
|
break; |
|
|
|
default: |
|
BaseClass::RunTask( pTask ); |
|
break; |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// The sniper throws away the circular list of old decoys when we restore. |
|
//----------------------------------------------------------------------------- |
|
int CProtoSniper::Restore( IRestore &restore ) |
|
{ |
|
ClearOldDecoys(); |
|
|
|
return BaseClass::Restore( restore ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// |
|
// |
|
//----------------------------------------------------------------------------- |
|
float CProtoSniper::MaxYawSpeed( void ) |
|
{ |
|
return 60; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CProtoSniper::PrescheduleThink( void ) |
|
{ |
|
BaseClass::PrescheduleThink(); |
|
|
|
// If a sweep target is set, keep asking the AI to sweep the target |
|
if( m_hSweepTarget != NULL ) |
|
{ |
|
if( m_bSweepHighestPriority || (!HasCondition( COND_CAN_RANGE_ATTACK1 ) && !HasCondition( COND_SNIPER_NO_SHOT ) ) ) |
|
{ |
|
SetCondition( COND_SNIPER_SWEEP_TARGET ); |
|
} |
|
} |
|
else |
|
{ |
|
ClearCondition( COND_SNIPER_SWEEP_TARGET ); |
|
} |
|
|
|
// Think faster if the beam is on, this gives the beam higher resolution. |
|
if( m_pBeam ) |
|
{ |
|
SetNextThink( gpGlobals->curtime + 0.03 ); |
|
} |
|
else |
|
{ |
|
SetNextThink( gpGlobals->curtime + 0.1f ); |
|
} |
|
|
|
// If the enemy has just stepped into view, or we've acquired a new enemy, |
|
// Record the last time we've seen the enemy as right now. |
|
// |
|
// If the enemy has been out of sight for a full second, mark him eluded. |
|
if( GetEnemy() != NULL ) |
|
{ |
|
if( gpGlobals->curtime - GetEnemies()->LastTimeSeen( GetEnemy() ) > 30 ) |
|
{ |
|
// Stop pestering enemies after 30 seconds of frustration. |
|
GetEnemies()->ClearMemory( GetEnemy() ); |
|
SetEnemy(NULL); |
|
} |
|
} |
|
|
|
// Suppress at the sound of danger. Incoming missiles, for example. |
|
if( HasCondition( COND_HEAR_DANGER ) ) |
|
{ |
|
SetCondition( COND_SNIPER_SUPPRESSED ); |
|
} |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
Vector CProtoSniper::EyePosition( void ) |
|
{ |
|
if( m_spawnflags & SF_SNIPER_HIDDEN ) |
|
{ |
|
return GetLocalOrigin(); |
|
} |
|
else |
|
{ |
|
return BaseClass::EyePosition(); |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
Vector CProtoSniper::DesiredBodyTarget( CBaseEntity *pTarget ) |
|
{ |
|
// By default, aim for the center |
|
Vector vecTarget = pTarget->WorldSpaceCenter(); |
|
|
|
float flTimeSinceLastMiss = gpGlobals->curtime - m_flTimeLastShotMissed; |
|
|
|
if( pTarget->GetFlags() & FL_CLIENT ) |
|
{ |
|
if( !BaseClass::FVisible( vecTarget ) ) |
|
{ |
|
// go to the player's eyes if his center is concealed. |
|
// Bump up an inch so the player's not looking straight down a beam. |
|
vecTarget = pTarget->EyePosition() + Vector( 0, 0, 1 ); |
|
} |
|
} |
|
else |
|
{ |
|
if( pTarget->Classify() == CLASS_HEADCRAB ) |
|
{ |
|
// Headcrabs are tiny inside their boxes. |
|
vecTarget = pTarget->GetAbsOrigin(); |
|
vecTarget.z += 4.0; |
|
} |
|
else if( !m_bShootZombiesInChest && pTarget->Classify() == CLASS_ZOMBIE ) |
|
{ |
|
if( flTimeSinceLastMiss > 0.0f && flTimeSinceLastMiss < 4.0f && hl2_episodic.GetBool() ) |
|
{ |
|
vecTarget = pTarget->BodyTarget( GetBulletOrigin(), false ); |
|
} |
|
else |
|
{ |
|
// Shoot zombies in the headcrab |
|
vecTarget = pTarget->HeadTarget( GetBulletOrigin() ); |
|
} |
|
} |
|
else if( pTarget->Classify() == CLASS_ANTLION ) |
|
{ |
|
// Shoot about a few inches above the origin. This makes it easy to hit antlions |
|
// even if they are on their backs. |
|
vecTarget = pTarget->GetAbsOrigin(); |
|
vecTarget.z += 18.0f; |
|
} |
|
else if( pTarget->Classify() == CLASS_EARTH_FAUNA ) |
|
{ |
|
// Shoot birds in the center |
|
} |
|
else |
|
{ |
|
// Shoot NPCs in the chest |
|
vecTarget.z += 8.0f; |
|
} |
|
} |
|
|
|
return vecTarget; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
Vector CProtoSniper::LeadTarget( CBaseEntity *pTarget ) |
|
{ |
|
float targetTime; |
|
float targetDist; |
|
//float adjustedShotDist; |
|
//float actualShotDist; |
|
Vector vecAdjustedShot; |
|
Vector vecTarget; |
|
trace_t tr; |
|
|
|
/* |
|
NDebugOverlay::EntityBounds(pTarget, |
|
255,255,0,96,0.1f); |
|
*/ |
|
if( sniperLines.GetBool() ) |
|
{ |
|
Msg("Sniper %s is targeting %s\n", GetDebugName(), pTarget ? pTarget->GetDebugName() : "nobody" ); |
|
} |
|
|
|
if( pTarget == NULL ) |
|
{ |
|
// no target |
|
return vec3_origin; |
|
} |
|
|
|
// Get target |
|
vecTarget = DesiredBodyTarget( pTarget ); |
|
|
|
// Get bullet time to target |
|
targetDist = (vecTarget - GetBulletOrigin() ).Length(); |
|
targetTime = targetDist / GetBulletSpeed(); |
|
|
|
// project target's velocity over that time. |
|
Vector vecVelocity = vec3_origin; |
|
|
|
if( pTarget->IsPlayer() || pTarget->Classify() == CLASS_MISSILE ) |
|
{ |
|
// This target is a client, who has an actual velocity. |
|
vecVelocity = pTarget->GetSmoothedVelocity(); |
|
|
|
// Slow the vertical velocity down a lot, or the sniper will |
|
// lead a jumping player by firing several feet above his head. |
|
// THIS may affect the sniper hitting a player that's ascending/descending |
|
// ladders. If so, we'll have to check for the player's ladder flag. |
|
if( pTarget->GetFlags() & FL_CLIENT ) |
|
{ |
|
vecVelocity.z *= 0.25; |
|
} |
|
} |
|
else |
|
{ |
|
if( pTarget->MyNPCPointer() && pTarget->MyNPCPointer()->GetNavType() == NAV_FLY ) |
|
{ |
|
// Take a flying monster's velocity directly. |
|
vecVelocity = pTarget->GetAbsVelocity(); |
|
} |
|
else |
|
{ |
|
// Have to build a velocity vector using the character's current groundspeed. |
|
CBaseAnimating *pAnimating; |
|
|
|
pAnimating = (CBaseAnimating *)pTarget; |
|
|
|
Assert( pAnimating != NULL ); |
|
|
|
QAngle vecAngle; |
|
vecAngle.y = pAnimating->GetSequenceMoveYaw( pAnimating->GetSequence() ); |
|
vecAngle.x = 0; |
|
vecAngle.z = 0; |
|
|
|
vecAngle.y += pTarget->GetLocalAngles().y; |
|
|
|
AngleVectors( vecAngle, &vecVelocity ); |
|
|
|
vecVelocity = vecVelocity * pAnimating->m_flGroundSpeed; |
|
} |
|
} |
|
|
|
if( m_iMisses > 0 && !FClassnameIs( pTarget, "npc_bullseye" ) ) |
|
{ |
|
// I'm supposed to miss this shot, so aim above the target's head. |
|
// BUT DON'T miss bullseyes, and don't count the shot. |
|
vecAdjustedShot = vecTarget; |
|
vecAdjustedShot.z += 16; |
|
|
|
m_iMisses--; |
|
|
|
// NDebugOverlay::Cross3D(vecAdjustedShot,12.0f,255,0,0,false,1); |
|
|
|
return vecAdjustedShot; |
|
} |
|
|
|
vecAdjustedShot = vecTarget + ( vecVelocity * targetTime ); |
|
|
|
// if the adjusted shot falls well short of the target, take the straight shot. |
|
// it's not very interesting for the bullet to hit something far away from the |
|
// target. (for instance, if a sign or ledge or something is between the player |
|
// and the sniper, and the sniper would hit this object if he tries to lead the player) |
|
|
|
// NDebugOverlay::Cross3D(vecAdjustedShot,12.0f,5,255,0,false,1); |
|
|
|
if( sniperLines.GetFloat() == 1.0f ) |
|
{ |
|
Vector vecBulletOrigin; |
|
vecBulletOrigin = GetBulletOrigin(); |
|
CPVSFilter filter( GetLocalOrigin() ); |
|
te->ShowLine( filter, 0.0, &vecBulletOrigin, &vecAdjustedShot ); |
|
} |
|
|
|
|
|
|
|
/* |
|
UTIL_TraceLine( vecBulletOrigin, vecAdjustedShot, MASK_SHOT, this, &tr ); |
|
|
|
actualShotDist = (tr.endpos - vecBulletOrigin ).Length(); |
|
adjustedShotDist = ( vecAdjustedShot - vecBulletOrigin ).Length(); |
|
|
|
///////////////////////////////////////////// |
|
// the shot taken should hit within 10% of the sniper's distance to projected target. |
|
// else, shoot straight. (there's some object in the way of the adjusted shot) |
|
///////////////////////////////////////////// |
|
if( actualShotDist <= adjustedShotDist * 0.9 ) |
|
{ |
|
vecAdjustedShot = vecTarget; |
|
} |
|
*/ |
|
return vecAdjustedShot; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
// Sniper killed the player. Pick the player's body or something |
|
// nearby to point the laser at, so that the player can get |
|
// a fix on the sniper's location. |
|
//--------------------------------------------------------- |
|
CBaseEntity *CProtoSniper::PickDeadPlayerTarget() |
|
{ |
|
const int iSearchSize = 32; |
|
CBaseEntity *pTarget = AI_GetSinglePlayer(); |
|
CBaseEntity *pEntities[ iSearchSize ]; |
|
|
|
int iNumEntities = UTIL_EntitiesInSphere( pEntities, iSearchSize, AI_GetSinglePlayer()->GetAbsOrigin(), 180.0f, 0 ); |
|
|
|
// Not very robust, but doesn't need to be. Randomly select a nearby object in the list that isn't an NPC. |
|
if( iNumEntities > 0 ) |
|
{ |
|
int i; |
|
|
|
// Try a few times to randomly select a target. |
|
for( i = 0 ; i < 10 ; i++ ) |
|
{ |
|
CBaseEntity *pCandidate = pEntities[ random->RandomInt(0, iNumEntities - 1) ]; |
|
|
|
if( !pCandidate->IsNPC() && FInViewCone(pCandidate) ) |
|
{ |
|
return pCandidate; |
|
} |
|
} |
|
} |
|
|
|
// Fall through to accept the player as a target. |
|
return pTarget; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CProtoSniper::InputEnableSniper( inputdata_t &inputdata ) |
|
{ |
|
ClearCondition( COND_SNIPER_DISABLED ); |
|
SetCondition( COND_SNIPER_ENABLED ); |
|
|
|
m_fEnabled = true; |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CProtoSniper::InputDisableSniper( inputdata_t &inputdata ) |
|
{ |
|
ClearCondition( COND_SNIPER_ENABLED ); |
|
SetCondition( COND_SNIPER_DISABLED ); |
|
|
|
m_fEnabled = false; |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
bool CProtoSniper::FindFrustratedShot( float flNoise ) |
|
{ |
|
Vector vecForward; |
|
Vector vecStart; |
|
Vector vecAimAt; |
|
Vector vecAim; |
|
|
|
if( !GetEnemy() ) |
|
{ |
|
return false; |
|
} |
|
|
|
// Just pick a spot somewhere around the target. |
|
// Try a handful of times to pick a spot that guarantees the |
|
// target will see the laser. |
|
#define MAX_TRIES 15 |
|
for( int i = 0 ; i < MAX_TRIES ; i++ ) |
|
{ |
|
Vector vecSpot = GetEnemyLKP(); |
|
|
|
vecSpot.x += random->RandomFloat( -64, 64 ); |
|
vecSpot.y += random->RandomFloat( -64, 64 ); |
|
vecSpot.z += random->RandomFloat( -40, 40 ); |
|
|
|
// Help move the frustrated spot off the target's BBOX in X/Y space. |
|
if( vecSpot.x < 0 ) |
|
vecSpot.x -= 32; |
|
else |
|
vecSpot.x += 32; |
|
|
|
if( vecSpot.y < 0 ) |
|
vecSpot.y -= 32; |
|
else |
|
vecSpot.y += 32; |
|
|
|
Vector vecSrc, vecDir; |
|
|
|
vecSrc = GetAbsOrigin(); |
|
vecDir = vecSpot - vecSrc; |
|
VectorNormalize( vecDir ); |
|
|
|
if( GetEnemy()->FVisible( vecSpot ) || i == MAX_TRIES - 1 ) |
|
{ |
|
trace_t tr; |
|
AI_TraceLine(vecSrc, vecSrc + vecDir * 8192, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr); |
|
|
|
if( !GetEnemy()->FVisible( tr.endpos ) ) |
|
{ |
|
// Dont accept this point unless we are out of tries! |
|
if( i != MAX_TRIES - 1 ) |
|
{ |
|
continue; |
|
} |
|
} |
|
m_vecFrustratedTarget = tr.endpos; |
|
break; |
|
} |
|
} |
|
|
|
#if 0 |
|
NDebugOverlay::Line(vecStart, tr.endpos, 0,255,0, true, 20 ); |
|
#endif |
|
|
|
return true; |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
// See all NPC's easily. |
|
// |
|
// Only see the player if you can trace to both of his |
|
// eyeballs. That is, allow the player to peek around corners. |
|
// This is a little more expensive than the base class' check! |
|
//--------------------------------------------------------- |
|
#define SNIPER_EYE_DIST 0.75 |
|
#define SNIPER_TARGET_VERTICAL_OFFSET Vector( 0, 0, 5 ); |
|
bool CProtoSniper::FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker ) |
|
{ |
|
if( m_spawnflags & SF_SNIPER_VIEWCONE ) |
|
{ |
|
// Viewcone snipers are blind with their laser off. |
|
if( !IsLaserOn() ) |
|
{ |
|
return false; |
|
} |
|
} |
|
|
|
if( !pEntity->IsPlayer() ) |
|
{ |
|
// NPC |
|
return BaseClass::FVisible( pEntity, traceMask, ppBlocker ); |
|
} |
|
|
|
if ( pEntity->GetFlags() & FL_NOTARGET ) |
|
{ |
|
return false; |
|
} |
|
|
|
Vector vecVerticalOffset; |
|
Vector vecRight; |
|
Vector vecEye; |
|
trace_t tr; |
|
|
|
if( fabs( GetAbsOrigin().z - pEntity->WorldSpaceCenter().z ) <= 120.f ) |
|
{ |
|
// If the player is around the same elevation, look straight at his eyes. |
|
// At the same elevation, the vertical peeking allowance makes it too easy |
|
// for a player to dispatch the sniper from cover. |
|
vecVerticalOffset = vec3_origin; |
|
} |
|
else |
|
{ |
|
// Otherwise, look at a spot below his eyes. This allows the player to back away |
|
// from his cover a bit and have a peek at the sniper without being detected. |
|
vecVerticalOffset = SNIPER_TARGET_VERTICAL_OFFSET; |
|
} |
|
|
|
AngleVectors( pEntity->GetLocalAngles(), NULL, &vecRight, NULL ); |
|
|
|
vecEye = vecRight * SNIPER_EYE_DIST - vecVerticalOffset; |
|
UTIL_TraceLine( EyePosition(), pEntity->EyePosition() + vecEye, MASK_BLOCKLOS, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
#if 0 |
|
NDebugOverlay::Line(EyePosition(), tr.endpos, 0,255,0, true, 0.1); |
|
#endif |
|
|
|
bool fCheckFailed = false; |
|
|
|
if( tr.fraction != 1.0 ) |
|
{ |
|
fCheckFailed = true; |
|
} |
|
|
|
// Don't check the other eye if the first eye failed. |
|
if( !fCheckFailed ) |
|
{ |
|
vecEye = -vecRight * SNIPER_EYE_DIST - vecVerticalOffset; |
|
UTIL_TraceLine( EyePosition(), pEntity->EyePosition() + vecEye, MASK_BLOCKLOS, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
#if 0 |
|
NDebugOverlay::Line(EyePosition(), tr.endpos, 0,255,0, true, 0.1); |
|
#endif |
|
|
|
if( tr.fraction != 1.0 ) |
|
{ |
|
fCheckFailed = true; |
|
} |
|
} |
|
|
|
if( !fCheckFailed ) |
|
{ |
|
// Can see the player. |
|
return true; |
|
} |
|
|
|
// Now, if the check failed, see if the player is ducking and has recently |
|
// fired a muzzleflash. If yes, see if you'd be able to see the player if |
|
// they were standing in their current position instead of ducking. Since |
|
// the sniper doesn't have a clear shot in this situation, he will harrass |
|
// near the player. |
|
CBasePlayer *pPlayer; |
|
|
|
pPlayer = ToBasePlayer( pEntity ); |
|
|
|
if( (pPlayer->GetFlags() & FL_DUCKING) && pPlayer->MuzzleFlashTime() > gpGlobals->curtime ) |
|
{ |
|
vecEye = pPlayer->EyePosition() + Vector( 0, 0, 32 ); |
|
UTIL_TraceLine( EyePosition(), vecEye, MASK_BLOCKLOS, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
if( tr.fraction != 1.0 ) |
|
{ |
|
// Everything failed. |
|
if (ppBlocker) |
|
{ |
|
*ppBlocker = tr.m_pEnt; |
|
} |
|
return false; |
|
} |
|
else |
|
{ |
|
// Fake being able to see the player. |
|
return true; |
|
} |
|
} |
|
|
|
if (ppBlocker) |
|
{ |
|
*ppBlocker = tr.m_pEnt; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Draw any debug text overlays |
|
// Output : Returns the current text offset from the top |
|
//----------------------------------------------------------------------------- |
|
int CProtoSniper::DrawDebugTextOverlays() |
|
{ |
|
int text_offset = 0; |
|
|
|
// --------------------- |
|
// Print Baseclass text |
|
// --------------------- |
|
text_offset = BaseClass::DrawDebugTextOverlays(); |
|
|
|
if (m_debugOverlays & OVERLAY_TEXT_BIT) |
|
{ |
|
char tempstr[512]; |
|
|
|
CSniperTarget *pTarget = NULL; |
|
if ( m_iNumGroupTargets > 0 ) |
|
{ |
|
pTarget = dynamic_cast<CSniperTarget *>(m_pGroupTarget[0]); |
|
} |
|
|
|
Q_snprintf( tempstr, sizeof( tempstr ), "Sweep group (count): %s (%d)", pTarget != NULL ? STRING( pTarget->m_iszGroupName ) : "<None>", m_iNumGroupTargets ); |
|
EntityText( text_offset, tempstr, 0 ); |
|
text_offset++; |
|
|
|
for ( int i = 0; i < m_iNumGroupTargets; i++ ) |
|
{ |
|
if ( m_pGroupTarget[i] != NULL ) |
|
{ |
|
NDebugOverlay::VertArrow( EyePosition(), m_pGroupTarget[i]->GetAbsOrigin(), 8, 0, 255, 0, 0, true, 0); |
|
} |
|
} |
|
} |
|
|
|
return text_offset; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Inform the sniper that a bullet missed its intended target. We don't know |
|
// which bullet or which target. |
|
//----------------------------------------------------------------------------- |
|
void CProtoSniper::NotifyShotMissedTarget() |
|
{ |
|
m_flTimeLastShotMissed = gpGlobals->curtime; |
|
// In episodic, aim at the (easier to hit at distance or high speed) centers |
|
// of the bodies of NPC targets. This change makes Alyx sniper less likely to |
|
// miss zombie and zombines over and over because of the large amount of head movement |
|
// in these NPCs' walk and run animations. |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
// Schedules |
|
// |
|
//----------------------------------------------------------------------------- |
|
|
|
AI_BEGIN_CUSTOM_NPC( proto_sniper, CProtoSniper ) |
|
|
|
DECLARE_CONDITION( COND_SNIPER_CANATTACKDECOY ); |
|
DECLARE_CONDITION( COND_SNIPER_SUPPRESSED ); |
|
DECLARE_CONDITION( COND_SNIPER_ENABLED ); |
|
DECLARE_CONDITION( COND_SNIPER_DISABLED ); |
|
DECLARE_CONDITION( COND_SNIPER_FRUSTRATED ); |
|
DECLARE_CONDITION( COND_SNIPER_SWEEP_TARGET ); |
|
DECLARE_CONDITION( COND_SNIPER_NO_SHOT ); |
|
|
|
DECLARE_TASK( TASK_SNIPER_FRUSTRATED_ATTACK ); |
|
DECLARE_TASK( TASK_SNIPER_PAINT_ENEMY ); |
|
DECLARE_TASK( TASK_SNIPER_PAINT_DECOY ); |
|
DECLARE_TASK( TASK_SNIPER_PAINT_FRUSTRATED ); |
|
DECLARE_TASK( TASK_SNIPER_PAINT_SWEEP_TARGET ); |
|
DECLARE_TASK( TASK_SNIPER_ATTACK_CURSOR ); |
|
DECLARE_TASK( TASK_SNIPER_PAINT_NO_SHOT ); |
|
DECLARE_TASK( TASK_SNIPER_PLAYER_DEAD ); |
|
|
|
//========================================================= |
|
// SCAN |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_SCAN, |
|
|
|
" Tasks" |
|
" TASK_WAIT_INDEFINITE 0" |
|
" " |
|
" Interrupts" |
|
" COND_NEW_ENEMY" |
|
" COND_ENEMY_DEAD" |
|
" COND_HEAR_DANGER" |
|
" COND_SNIPER_DISABLED" |
|
" COND_SNIPER_SWEEP_TARGET" |
|
) |
|
|
|
//========================================================= |
|
// CAMP |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_CAMP, |
|
|
|
" Tasks" |
|
" TASK_WAIT_INDEFINITE 0" |
|
" " |
|
" Interrupts" |
|
" COND_NEW_ENEMY" |
|
" COND_ENEMY_DEAD" |
|
" COND_CAN_RANGE_ATTACK1" |
|
" COND_SNIPER_CANATTACKDECOY" |
|
" COND_SNIPER_SUPPRESSED" |
|
" COND_HEAR_DANGER" |
|
" COND_SNIPER_DISABLED" |
|
" COND_SNIPER_FRUSTRATED" |
|
" COND_SNIPER_SWEEP_TARGET" |
|
) |
|
|
|
//========================================================= |
|
// ATTACK |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_ATTACK, |
|
|
|
" Tasks" |
|
" TASK_SNIPER_PAINT_ENEMY 0" |
|
" TASK_RANGE_ATTACK1 0" |
|
" " |
|
" Interrupts" |
|
" COND_ENEMY_OCCLUDED" |
|
" COND_ENEMY_DEAD" |
|
" COND_HEAR_DANGER" |
|
" COND_SNIPER_DISABLED" |
|
) |
|
|
|
//========================================================= |
|
// ATTACK |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_SNAPATTACK, |
|
|
|
" Tasks" |
|
" TASK_SNIPER_ATTACK_CURSOR 0" |
|
" " |
|
" Interrupts" |
|
" COND_ENEMY_OCCLUDED" |
|
" COND_ENEMY_DEAD" |
|
" COND_NEW_ENEMY" |
|
" COND_HEAR_DANGER" |
|
" COND_SNIPER_DISABLED" |
|
) |
|
|
|
//========================================================= |
|
// RELOAD |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_RELOAD, |
|
|
|
" Tasks" |
|
" TASK_RELOAD 0" |
|
" TASK_WAIT 1.0" |
|
" " |
|
" Interrupts" |
|
" COND_HEAR_DANGER" |
|
) |
|
|
|
//========================================================= |
|
// Attack decoy |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_ATTACKDECOY, |
|
|
|
" Tasks" |
|
" TASK_SNIPER_PAINT_DECOY 2.0" |
|
" TASK_RANGE_ATTACK2 0" |
|
" " |
|
" Interrupts" |
|
" COND_NEW_ENEMY" |
|
" COND_ENEMY_DEAD" |
|
" COND_HEAR_DANGER" |
|
" COND_CAN_RANGE_ATTACK1" |
|
" COND_SNIPER_DISABLED" |
|
" COND_SNIPER_SWEEP_TARGET" |
|
) |
|
|
|
//========================================================= |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_SUPPRESSED, |
|
|
|
" Tasks" |
|
" TASK_WAIT 2.0" |
|
" " |
|
" Interrupts" |
|
) |
|
|
|
//========================================================= |
|
// Sniper is allowed to process a couple conditions while |
|
// disabled, but mostly he waits until he's enabled. |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_DISABLEDWAIT, |
|
|
|
" Tasks" |
|
" TASK_WAIT 0.5" |
|
" " |
|
" Interrupts" |
|
" COND_SNIPER_ENABLED" |
|
" COND_NEW_ENEMY" |
|
" COND_ENEMY_DEAD" |
|
) |
|
|
|
//========================================================= |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_FRUSTRATED_ATTACK, |
|
|
|
" Tasks" |
|
" TASK_WAIT 2.0" |
|
" TASK_SNIPER_PAINT_FRUSTRATED 0.05" |
|
" TASK_SNIPER_PAINT_FRUSTRATED 0.025" |
|
" TASK_SNIPER_PAINT_FRUSTRATED 0.0" |
|
" TASK_SNIPER_FRUSTRATED_ATTACK 0.0" |
|
" " |
|
" Interrupts" |
|
" COND_NEW_ENEMY" |
|
" COND_ENEMY_DEAD" |
|
" COND_SNIPER_DISABLED" |
|
" COND_CAN_RANGE_ATTACK1" |
|
" COND_SEE_ENEMY" |
|
" COND_HEAR_DANGER" |
|
" COND_SNIPER_SWEEP_TARGET" |
|
) |
|
|
|
//========================================================= |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_SWEEP_TARGET, |
|
|
|
" Tasks" |
|
" TASK_SNIPER_PAINT_SWEEP_TARGET 0.0" |
|
" " |
|
" Interrupts" |
|
" COND_NEW_ENEMY" |
|
" COND_SNIPER_DISABLED" |
|
" COND_CAN_RANGE_ATTACK1" |
|
" COND_HEAR_DANGER" |
|
" COND_SNIPER_NO_SHOT" |
|
) |
|
|
|
//========================================================= |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_SWEEP_TARGET_NOINTERRUPT, |
|
|
|
" Tasks" |
|
" TASK_SNIPER_PAINT_SWEEP_TARGET 0.0" |
|
" " |
|
" Interrupts" |
|
" COND_SNIPER_DISABLED" |
|
) |
|
|
|
//========================================================= |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_NO_CLEAR_SHOT, |
|
|
|
" Tasks" |
|
" TASK_SNIPER_PAINT_NO_SHOT 0.0" |
|
" TASK_SNIPER_PAINT_NO_SHOT 0.075" |
|
" TASK_SNIPER_PAINT_NO_SHOT 0.05" |
|
" TASK_SNIPER_PAINT_NO_SHOT 0.0" |
|
" " |
|
" Interrupts" |
|
" COND_NEW_ENEMY" |
|
" COND_ENEMY_DEAD" |
|
" COND_SNIPER_DISABLED" |
|
" COND_CAN_RANGE_ATTACK1" |
|
" COND_HEAR_DANGER" |
|
) |
|
|
|
//========================================================= |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_PSNIPER_PLAYER_DEAD, |
|
|
|
" Tasks" |
|
" TASK_SNIPER_PLAYER_DEAD 0" |
|
" " |
|
" Interrupts" |
|
) |
|
|
|
AI_END_CUSTOM_NPC() |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
// Sniper Bullet |
|
// |
|
//----------------------------------------------------------------------------- |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CSniperBullet::Precache() |
|
{ |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CSniperBullet::BulletThink( void ) |
|
{ |
|
// Set the bullet up to think again. |
|
SetNextThink( gpGlobals->curtime + 0.05 ); |
|
|
|
if( !GetOwnerEntity() ) |
|
{ |
|
// Owner died! |
|
Stop(); |
|
return; |
|
} |
|
|
|
if( gpGlobals->curtime >= m_SoundTime ) |
|
{ |
|
// See if it's time to make the sonic boom. |
|
CPASAttenuationFilter filter( this, ATTN_NONE ); |
|
EmitSound( filter, entindex(), "NPC_Sniper.SonicBoom" ); |
|
|
|
if( GetOwnerEntity() ) |
|
{ |
|
CAI_BaseNPC *pSniper; |
|
CAI_BaseNPC *pEnemyNPC; |
|
pSniper = GetOwnerEntity()->MyNPCPointer(); |
|
|
|
if( pSniper && pSniper->GetEnemy() ) |
|
{ |
|
pEnemyNPC = pSniper->GetEnemy()->MyNPCPointer(); |
|
|
|
// Warn my enemy if they can see the sniper. |
|
if( pEnemyNPC && GetOwnerEntity() && pEnemyNPC->FVisible( GetOwnerEntity()->WorldSpaceCenter() ) ) |
|
{ |
|
CSoundEnt::InsertSound( SOUND_DANGER | SOUND_CONTEXT_FROM_SNIPER, pSniper->GetEnemy()->EarPosition(), 16, 1.0f, GetOwnerEntity() ); |
|
} |
|
} |
|
} |
|
|
|
// No way the bullet will live this long. |
|
m_SoundTime = 1e9; |
|
} |
|
|
|
// Trace this timeslice of the bullet. |
|
Vector vecStart; |
|
Vector vecEnd; |
|
float flInterval; |
|
|
|
flInterval = gpGlobals->curtime - GetLastThink(); |
|
vecStart = GetAbsOrigin(); |
|
vecEnd = vecStart + ( m_vecDir * (m_Speed * flInterval) ); |
|
float flDist = (vecStart - vecEnd).Length(); |
|
|
|
//Msg("."); |
|
|
|
trace_t tr; |
|
AI_TraceLine( vecStart, vecEnd, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
if( tr.fraction != 1.0 ) |
|
{ |
|
// This slice of bullet will hit something. |
|
GetOwnerEntity()->FireBullets( 1, vecStart, m_vecDir, vec3_origin, flDist, m_AmmoType, 0 ); |
|
m_iImpacts++; |
|
|
|
#ifdef HL2_EPISODIC |
|
if( tr.m_pEnt->IsNPC() || m_iImpacts == NUM_PENETRATIONS ) |
|
#else |
|
if( tr.m_pEnt->m_takedamage == DAMAGE_YES || m_iImpacts == NUM_PENETRATIONS ) |
|
#endif//HL2_EPISODIC |
|
{ |
|
// Bullet stops when it hits an NPC, or when it has penetrated enough times. |
|
|
|
if( tr.m_pEnt && tr.m_pEnt->VPhysicsGetObject() ) |
|
{ |
|
if( tr.m_pEnt->VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) |
|
{ |
|
Pickup_ForcePlayerToDropThisObject(tr.m_pEnt); |
|
} |
|
} |
|
|
|
Stop(); |
|
return; |
|
} |
|
else |
|
{ |
|
#define STEP_SIZE 2 |
|
#define NUM_STEPS 6 |
|
// Try to slide a 'cursor' through the object that was hit. |
|
Vector vecCursor = tr.endpos; |
|
|
|
for( int i = 0 ; i < NUM_STEPS ; i++ ) |
|
{ |
|
//Msg("-"); |
|
vecCursor += m_vecDir * STEP_SIZE; |
|
|
|
if( UTIL_PointContents( vecCursor ) != CONTENTS_SOLID ) |
|
{ |
|
// Passed out of a solid! |
|
SetAbsOrigin( vecCursor ); |
|
|
|
// Fire another tracer. |
|
AI_TraceLine( vecCursor, vecCursor + m_vecDir * 8192, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); |
|
UTIL_Tracer( vecCursor, tr.endpos, 0, TRACER_DONT_USE_ATTACHMENT, m_Speed, true, "StriderTracer" ); |
|
return; |
|
} |
|
} |
|
|
|
// Bullet also stops when it fails to exit material after penetrating this far. |
|
//Msg("#\n"); |
|
if( m_bDirectShot ) |
|
{ |
|
CProtoSniper *pSniper = dynamic_cast<CProtoSniper*>(GetOwnerEntity()); |
|
if( pSniper ) |
|
{ |
|
pSniper->NotifyShotMissedTarget(); |
|
} |
|
} |
|
|
|
Stop(); |
|
return; |
|
} |
|
} |
|
else |
|
{ |
|
SetAbsOrigin( vecEnd ); |
|
} |
|
} |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CSniperBullet::Start( const Vector &vecOrigin, const Vector &vecTarget, CBaseEntity *pOwner, bool bDirectShot ) |
|
{ |
|
m_flLastThink = gpGlobals->curtime; |
|
|
|
if( m_AmmoType == -1 ) |
|
{ |
|
// This guy doesn't have a REAL weapon, per say, but he does fire |
|
// sniper rounds. Since there's no weapon to index the ammo type, |
|
// do it manually here. |
|
m_AmmoType = GetAmmoDef()->Index("SniperRound"); |
|
|
|
// This is the bullet that is used for all subsequent FireBullets() calls after the first |
|
// call penetrates a surface and keeps going. |
|
m_PenetratedAmmoType = GetAmmoDef()->Index("SniperPenetratedRound"); |
|
} |
|
|
|
if( m_fActive ) |
|
{ |
|
return false; |
|
} |
|
|
|
SetOwnerEntity( pOwner ); |
|
|
|
UTIL_SetOrigin( this, vecOrigin ); |
|
|
|
m_vecDir = vecTarget - vecOrigin; |
|
VectorNormalize( m_vecDir ); |
|
|
|
// Set speed; |
|
CProtoSniper *pSniper = dynamic_cast<CProtoSniper*>(pOwner); |
|
|
|
if( pSniper ) |
|
{ |
|
m_Speed = pSniper->GetBulletSpeed(); |
|
} |
|
else |
|
{ |
|
m_Speed = bulletSpeed.GetFloat(); |
|
} |
|
|
|
// Start the tracer here, and tell it to end at the end of the last trace |
|
// the trace comes from the loop above that does penetration. |
|
trace_t tr; |
|
UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + m_vecDir * 8192, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); |
|
UTIL_Tracer( vecOrigin, tr.endpos, 0, TRACER_DONT_USE_ATTACHMENT, m_Speed, true, "StriderTracer" ); |
|
|
|
float flElapsedTime = ( (tr.startpos - tr.endpos).Length() / m_Speed ); |
|
m_SoundTime = gpGlobals->curtime + flElapsedTime * 0.5; |
|
|
|
SetThink( &CSniperBullet::BulletThink ); |
|
SetNextThink( gpGlobals->curtime ); |
|
m_fActive = true; |
|
m_bDirectShot = bDirectShot; |
|
return true; |
|
|
|
/* |
|
int i; |
|
|
|
// Try to find all of the things the bullet can go through along the way. |
|
//------------------------------- |
|
//------------------------------- |
|
m_vecDir = vecTarget - vecOrigin; |
|
VectorNormalize( m_vecDir ); |
|
|
|
trace_t tr; |
|
|
|
|
|
// Elapsed time counts how long the bullet is in motion through this simulation. |
|
float flElapsedTime = 0; |
|
|
|
for( i = 0 ; i < NUM_PENETRATIONS ; i++ ) |
|
{ |
|
// Trace to the target. |
|
UTIL_TraceLine( GetAbsOrigin(), vecTarget, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
flShotDist = (tr.endpos - GetAbsOrigin()).Length(); |
|
|
|
// Record the two endpoints of the segment and the time at which this bullet hits, |
|
// and the time at which it's supposed to hit its mark. |
|
m_ImpactTime[ i ] = flElapsedTime + ( flShotDist / GetBulletSpeed() ); |
|
m_vecStart[ i ] = tr.startpos; |
|
m_vecEnd[ i ] = tr.endpos; |
|
|
|
// The elapsed time is now pushed forward by how long it takes the bullet |
|
// to travel through this segment. |
|
flElapsedTime += ( flShotDist / GetBulletSpeed() ); |
|
|
|
// Never let gpGlobals->curtime get added to the elapsed time! |
|
m_ImpactTime[ i ] += gpGlobals->curtime; |
|
|
|
CBaseEntity *pEnt; |
|
|
|
pEnt = tr.m_pEnt; |
|
|
|
if( !pEnt || |
|
pEnt->MyNPCPointer() || |
|
UTIL_DistApprox2D( tr.endpos, vecTarget ) <= 4 || |
|
FClassnameIs( pEnt, "prop_physics" ) ) |
|
{ |
|
// If we're close to the target, assume the shot is going to hit |
|
// the target and stop penetrating. |
|
// |
|
// If we're going to hit an NPC, stop penetrating. |
|
// |
|
// If we hit a physics prop, stop penetrating. |
|
// |
|
// Otherwise, keep looping. |
|
break; |
|
} |
|
|
|
// We're going to try to penetrate whatever the bullet has hit. |
|
|
|
// Push through the object by the penetration distance, then trace back. |
|
Vector vecCursor; |
|
|
|
vecCursor = tr.endpos; |
|
vecCursor += m_vecDir * PENETRATION_THICKNESS; |
|
|
|
UTIL_TraceLine( vecCursor, vecCursor + m_vecDir * -2, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
#if 1 |
|
if( tr.startsolid ) |
|
{ |
|
// The cursor is inside the solid. Solid is too thick to penetrate. |
|
#ifdef SNIPER_DEBUG |
|
Msg( "SNIPER STARTSOLID\n" ); |
|
#endif |
|
break; |
|
} |
|
#endif |
|
|
|
// Now put the bullet at this point and continue. |
|
UTIL_SetOrigin( this, vecCursor ); |
|
} |
|
//------------------------------- |
|
//------------------------------- |
|
*/ |
|
|
|
|
|
/* |
|
#ifdef SNIPER_DEBUG |
|
Msg( "PENETRATING %d items", i ); |
|
#endif // SNIPER_DEBUG |
|
|
|
#ifdef SNIPER_DEBUG |
|
Msg( "Dist: %f Travel Time: %f\n", flShotDist, m_ImpactTime ); |
|
#endif // SNIPER_DEBUG |
|
*/ |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CSniperBullet::Init( void ) |
|
{ |
|
#ifdef SNIPER_DEBUG |
|
Msg( "Bullet stopped\n" ); |
|
#endif // SNIPER_DEBUG |
|
|
|
m_fActive = false; |
|
m_vecDir.Init(); |
|
m_AmmoType = -1; |
|
m_SoundTime = 1e9; |
|
m_iImpacts = 0; |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CSniperBullet::Stop( void ) |
|
{ |
|
// The bullet doesn't retire immediately because it still has a sound |
|
// in the world that is relying on the bullet's position as a react origin. |
|
// So stick around for another second or so. |
|
SetThink( &CBaseEntity::SUB_Remove ); |
|
SetNextThink( gpGlobals->curtime + 1.0 ); |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
bool CSniperTarget::KeyValue( const char *szKeyName, const char *szValue ) |
|
{ |
|
if (FStrEq(szKeyName, "groupname")) |
|
{ |
|
m_iszGroupName = AllocPooledString( szValue ); |
|
return true; |
|
} |
|
else |
|
{ |
|
return CPointEntity::KeyValue( szKeyName, szValue ); |
|
} |
|
} |
|
|
|
LINK_ENTITY_TO_CLASS( info_snipertarget, CSniperTarget ); |
|
|
|
|
|
|