//========= Copyright 1996-2005, 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.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. // 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 #define SNIPER_PAINT_ENEMY_TIME 1.0f #define SNIPER_SUBSEQUENT_PAINT_TIME ( ( IsXbox() ) ? 1.0f : 0.4f ) #define SNIPER_PAINT_NPC_TIME_NOISE 0.75f #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_OPAQUE, 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 ); void LaserOff( void ); void LaserOn( const Vector &vecTarget, const Vector &vecDeviance ); void PaintTarget( const Vector &vecTarget, float flPaintTime ); bool IsPlayerAllySniper(); private: // 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; 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_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 ), // 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 ) { #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 = m_vecPaintStart - 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, P, &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(); } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- 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(); } //----------------------------------------------------------------------------- // 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; } //----------------------------------------------------------------------------- // 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; pGib = CreateRagGib( "models/combine_soldier.mdl", GetLocalOrigin(), GetLocalAngles(), (vecForward * flForce) + Vector(0, 0, 600), flFadeTime ); if( IsOnFire() || hl2_episodic.GetBool() ) { CBaseAnimating *pBase; pBase = dynamic_cast<CBaseAnimating*>(pGib); // Light up the gib. if( pBase ) { pBase->Ignite( 10, false ); } } } 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->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_OPAQUE 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_OPAQUE, 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_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: 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( SNIPER_PAINT_ENEMY_TIME + delay ); m_flPaintTime = SNIPER_PAINT_ENEMY_TIME + delay; } } else { m_flPaintTime = SNIPER_PAINT_ENEMY_TIME + random->RandomFloat( 0, SNIPER_PAINT_NPC_TIME_NOISE ); if( IsFastSniper() ) { // Get the shot off a little faster. m_flPaintTime *= 0.75f; } SetWait( m_flPaintTime ); } Vector vecCursor; // 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 ) ) { if( GetEnemy() && GetEnemy()->IsPlayer() ) { m_flTimeLastAttackedPlayer = gpGlobals->curtime; } TaskComplete(); } break; case TASK_SNIPER_FRUSTRATED_ATTACK: if( FireBullet( m_vecFrustratedTarget, false ) ) { TaskComplete(); } break; case TASK_SNIPER_PAINT_SWEEP_TARGET: 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( 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; 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--; 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) Vector vecBulletOrigin; vecBulletOrigin = GetBulletOrigin(); if( sniperLines.GetFloat() == 1.0f ) { 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_OPAQUE, 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_OPAQUE, 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_OPAQUE, 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 );
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 5821 | Knut Wikstrom |
Added Valve Source code. This is NOT to be commited to other than new code from Valve. |