diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/hl2/npc_combinegunship.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/server/hl2/npc_combinegunship.cpp')
| -rw-r--r-- | game/server/hl2/npc_combinegunship.cpp | 3228 |
1 files changed, 3228 insertions, 0 deletions
diff --git a/game/server/hl2/npc_combinegunship.cpp b/game/server/hl2/npc_combinegunship.cpp new file mode 100644 index 0000000..2583105 --- /dev/null +++ b/game/server/hl2/npc_combinegunship.cpp @@ -0,0 +1,3228 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "baseanimating.h" +#include "ai_network.h" +#include "ai_default.h" +#include "ai_schedule.h" +#include "ai_hull.h" +#include "ai_node.h" +#include "ai_task.h" +#include "ai_motor.h" +#include "entitylist.h" +#include "basecombatweapon.h" +#include "soundenvelope.h" +#include "gib.h" +#include "gamerules.h" +#include "ammodef.h" +#include "cbasehelicopter.h" +#include "npcevent.h" +#include "ndebugoverlay.h" +#include "decals.h" +#include "explode.h" // temp (sjb) +#include "smoke_trail.h" // temp (sjb) +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ar2_explosion.h" +#include "te_effect_dispatch.h" +#include "rope.h" +#include "effect_dispatch_data.h" +#include "trains.h" +#include "globals.h" +#include "physics_prop_ragdoll.h" +#include "iservervehicle.h" +#include "soundent.h" +#include "npc_citizen17.h" +#include "physics_saverestore.h" +#include "hl2_shareddefs.h" +#include "props.h" +#include "npc_attackchopper.h" +#include "citadel_effects_shared.h" +#include "eventqueue.h" +#include "beam_flags.h" +#include "ai_eventresponse.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define GUNSHIP_MSG_BIG_SHOT 1 +#define GUNSHIP_MSG_STREAKS 2 + +#define GUNSHIP_NUM_DAMAGE_OUTPUTS 4 + +extern short g_sModelIndexFireball; // holds the index for the fireball + +int g_iGunshipEffectIndex = -1; + +#define GUNSHIP_ACCEL_RATE 500 + +// Spawnflags +#define SF_GUNSHIP_NO_GROUND_ATTACK ( 1 << 12 ) +#define SF_GUNSHIP_USE_CHOPPER_MODEL ( 1 << 13 ) + +ConVar sk_gunship_burst_size("sk_gunship_burst_size", "15" ); +ConVar sk_gunship_burst_min("sk_gunship_burst_min", "800" ); +ConVar sk_gunship_burst_dist("sk_gunship_burst_dist", "768" ); + +// Number of times the gunship must be struck by explosive damage +ConVar sk_gunship_health_increments( "sk_gunship_health_increments", "0" ); + +/* + +Wedge's notes: + + Gunship should move its head according to flight model when the target is behind the gunship, + or when the target is too far away to shoot at. Otherwise, the head should aim at the target. + + Negative angvelocity.y is a RIGHT turn. + Negative angvelocity.x is UP + +*/ + +#define GUNSHIP_AP_MUZZLE 5 + +#define GUNSHIP_MAX_SPEED 1056.0f + +#define GUNSHIP_MAX_FIRING_SPEED 200.0f +#define GUNSHIP_MIN_ROCKET_DIST 1000.0f +#define GUNSHIP_MAX_GUN_DIST 2000.0f +#define GUNSHIP_ARRIVE_DIST 128.0f + +#define GUNSHIP_HOVER_SPEED 300.0f // play hover animation if moving slower than this. + +#define GUNSHIP_AE_THRUST 1 + +#define GUNSHIP_HEAD_MAX_UP -65 +#define GUNSHIP_HEAD_MAX_DOWN 60 +#define GUNSHIP_HEAD_MAX_LEFT 60 +#define GUNSHIP_HEAD_MAX_RIGHT -60 + +#define BASE_STITCH_VELOCITY 800 //Units per second +#define MAX_STITCH_VELOCITY 1000 //Units per second + +#define GUNSHIP_LEAD_DISTANCE 800.0f +#define GUNSHIP_AVOID_DIST 512.0f +#define GUNSHIP_STITCH_MIN 512.0f + +#define GUNSHIP_MIN_CHASE_DIST_DIFF 128.0f // Distance threshold used to determine when a target has moved enough to update our navigation to it + +#define MIN_GROUND_ATTACK_DIST 500.0f // Minimum distance a target has to be for the gunship to consider using the ground attack weapon +#define MIN_GROUND_ATTACK_HEIGHT_DIFF 128.0f // Target's position and hit position must be within this threshold vertically + +#define GUNSHIP_WASH_ALTITUDE 1024.0f + +#define GUNSHIP_MIN_DAMAGE_THRESHOLD 50.0f + +#define GUNSHIP_INNER_NAV_DIST 400.0f +#define GUNSHIP_OUTER_NAV_DIST 800.0f + +#define GUNSHIP_BELLYBLAST_TARGET_HEIGHT 512.0 // Height above targets that the gunship wants to be when bellyblasting + +#define GUNSHIP_MISSILE_MAX_RESPONSE_TIME 0.4 +#define GUNSHIP_MAX_HITS_PER_BURST 5 + +#define GUNSHIP_FLARE_IGNORE_TIME 6.0 + +//===================================== +// Custom activities +//===================================== +Activity ACT_GUNSHIP_PATROL; +Activity ACT_GUNSHIP_HOVER; +Activity ACT_GUNSHIP_CRASH; + +#define GUNSHIP_DEBUG_LEADING 1 +#define GUNSHIP_DEBUG_PATH 2 +#define GUNSHIP_DEBUG_STITCHING 3 +#define GUNSHIP_DEBUG_BELLYBLAST 4 + +ConVar g_debug_gunship( "g_debug_gunship", "0", FCVAR_CHEAT ); + +//----------------------------------------------------------------------------- +// Purpose: Dying gunship ragdoll controller +//----------------------------------------------------------------------------- +class CGunshipRagdollMotion : public IMotionEvent +{ + DECLARE_SIMPLE_DATADESC(); +public: + virtual simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) + { + linear = Vector(0,0,400); + angular = Vector(0,600,100); + + return SIM_GLOBAL_ACCELERATION; + } +}; + +BEGIN_SIMPLE_DATADESC( CGunshipRagdollMotion ) +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CTargetGunshipCrash : public CPointEntity +{ + DECLARE_CLASS( CTargetGunshipCrash, CPointEntity ); +public: + DECLARE_DATADESC(); + + void InputEnable( inputdata_t &inputdata ) + { + m_bDisabled = false; + } + void InputDisable( inputdata_t &inputdata ) + { + m_bDisabled = true; + } + bool IsDisabled( void ) + { + return m_bDisabled; + } + void GunshipCrashedOnTarget( void ) + { + m_OnCrashed.FireOutput( this, this ); + } + +private: + bool m_bDisabled; + + COutputEvent m_OnCrashed; +}; + +LINK_ENTITY_TO_CLASS( info_target_gunshipcrash, CTargetGunshipCrash ); + +BEGIN_DATADESC( CTargetGunshipCrash ) + DEFINE_FIELD( m_bDisabled, FIELD_BOOLEAN ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + // Outputs + DEFINE_OUTPUT( m_OnCrashed, "OnCrashed" ), +END_DATADESC() + + +//=================================================================== +// Gunship - the combine dugongic like attack vehicle. +//=================================================================== +class CNPC_CombineGunship : public CBaseHelicopter +{ +public: + DECLARE_CLASS( CNPC_CombineGunship, CBaseHelicopter ); + + CNPC_CombineGunship( void ); + ~CNPC_CombineGunship( void ); + + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + DEFINE_CUSTOM_AI; + + void PlayPatrolLoop( void ); + void PlayAngryLoop( void ); + + void Spawn( void ); + void Precache( void ); + void OnRestore( void ); + void PrescheduleThink( void ); + void HelicopterPostThink( void ); + void StopLoopingSounds( void ); + + bool IsValidEnemy( CBaseEntity *pEnemy ); + void GatherEnemyConditions( CBaseEntity *pEnemy ); + + void Flight( void ); + + bool FVisible( CBaseEntity *pEntity, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ); + int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + void FireDamageOutputsUpto( int iDamageNumber ); + + virtual float GetAcceleration( void ) { return 15; } + + virtual void MakeTracer( const Vector &vecTracerSrc, const trace_t &tr, int iTracerType ); + virtual void DoImpactEffect( trace_t &tr, int nDamageType ); + + void MoveHead( void ); + void UpdateDesiredPosition( void ); + void DoCombat( void ); + bool ChooseEnemy( void ); + void DoMuzzleFlash( void ); + void Ping( void ); + + void FireCannonRound( void ); + + // Gunship death process + void Event_Killed( const CTakeDamageInfo &info ); + void BeginCrash( void ); // I'm going to go to a crash point and die there + void BeginDestruct( void ); // I want to die now, so create my ragdoll + void SelfDestruct( void ); // I'm now fully dead, so remove myself. + void CreateSmokeTrail( void ); + bool FindNearestGunshipCrash( void ); + + int BloodColor( void ) { return DONT_BLEED; } + void GibMonster( void ); + + void UpdateRotorSoundPitch( int iPitch ); + void InitializeRotorSound( void ); + + void ApplyGeneralDrag( void ); + void ApplySidewaysDrag( const Vector &vecRight ); + + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + + void UpdateEnemyTarget( void ); + + Vector GetEnemyTarget( void ); + Vector GetMissileTarget( void ); + + float GroundDistToPosition( const Vector &pos ); + + bool FireGun( void ); + bool IsTargettingMissile( void ); + + Class_T Classify( void ) { return CLASS_COMBINE_GUNSHIP; } // for now + float GetAutoAimRadius() { return 144.0f; } + + // Input functions + void InputSetPenetrationDepth( inputdata_t &inputdata ); + void InputOmniscientOn( inputdata_t &inputdata ); + void InputOmniscientOff( inputdata_t &inputdata ); + void InputBlindfireOn( inputdata_t &inputdata ); + void InputBlindfireOff( inputdata_t &inputdata ); + void InputSelfDestruct( inputdata_t &inputdata ); + void InputSetDockingBBox( inputdata_t &inputdata ); + void InputSetNormalBBox( inputdata_t &inputdata ); + void InputEnableGroundAttack( inputdata_t &inputdata ); + void InputDisableGroundAttack( inputdata_t &inputdata ); + void InputDoGroundAttack( inputdata_t &inputdata ); + + //NOTENOTE: I'm rather queasy about adding these, as they can lead to nasty bugs... + void InputBecomeInvulnerable( inputdata_t &inputdata ); + void InputBecomeVulnerable( inputdata_t &inputdata ); + + bool PoseGunTowardTargetDirection( const Vector &vTargetDir ); + void StartCannonBurst( int iBurstSize ); + void StopCannonBurst( void ); + + bool CheckGroundAttack( void ); + void StartGroundAttack( void ); + void StopGroundAttack( bool bDoAttack ); + Vector GetGroundAttackHitPosition( void ); + void DoGroundAttackExplosion( void ); + void DrawRotorWash( float flAltitude, const Vector &vecRotorOrigin ); + + void ManageWarningBeam( void ); + void DoBellyBlastDamage( trace_t &tr, Vector vMins, Vector vMaxs ); + + // Updates the facing direction + void UpdateFacingDirection( void ); + void CreateBellyBlastEnergyCore( void ); + +protected: + // Because the combine gunship is a leaf class, we can use + // static variables to store this information, and save some memory. + // Should the gunship end up having inheritors, their activate may + // stomp these numbers, in which case you should make these ordinary members + // again. + static int m_poseFlex_Horz, m_poseFlex_Vert, m_posePitch, m_poseYaw, m_poseFin_Accel, m_poseFin_Sway; + static int m_poseWeapon_Pitch, m_poseWeapon_Yaw; + + static bool m_sbStaticPoseParamsLoaded; + virtual void PopulatePoseParameters( void ); + +private: + // Outputs + COutputEvent m_OnFireCannon; + COutputEvent m_OnCrashed; + + COutputEvent m_OnFirstDamage; // First damage tick + COutputEvent m_OnSecondDamage; + COutputEvent m_OnThirdDamage; + COutputEvent m_OnFourthDamage; + // Keep track of which damage outputs we've fired. This is necessary + // to ensure that the game doesn't break if a mapmaker has outputs that + // must be fired on gunships, and the player switches skill levels + // midway through a gunship battle. + bool m_bDamageOutputsFired[GUNSHIP_NUM_DAMAGE_OUTPUTS]; + + float m_flNextGroundAttack; // Time to wait before the next ground attack + bool m_bIsGroundAttacking; // Denotes that we are ground attacking + bool m_bCanGroundAttack; // Denotes whether we can ground attack or not + float m_flGroundAttackTime; // Delay before blast happens from ground attack + + CHandle<SmokeTrail> m_pSmokeTrail; + EHANDLE m_hGroundAttackTarget; + + CSoundPatch *m_pAirExhaustSound; + CSoundPatch *m_pAirBlastSound; + CSoundPatch *m_pCannonSound; + + CBaseEntity *m_pRotorWashModel; + QAngle m_vecAngAcceleration; + + float m_flEndDestructTime; + + int m_iDoSmokePuff; + int m_iAmmoType; + int m_iBurstSize; + + bool m_fBlindfire; + bool m_fOmniscient; + bool m_bIsFiring; + int m_iBurstHits; + bool m_bPreFire; + bool m_bInvulnerable; + + float m_flTimeNextPing; + float m_flPenetrationDepth; + float m_flDeltaT; + float m_flTimeNextAttack; + float m_flNextSeeEnemySound; + float m_flNextRocket; + float m_flBurstDelay; + + Vector m_vecAttackPosition; + Vector m_vecAttackVelocity; + + // Used when the gunships using the chopper model + Vector m_angGun; + + // For my death throes + IPhysicsMotionController *m_pCrashingController; + CGunshipRagdollMotion m_crashCallback; + EHANDLE m_hRagdoll; + CHandle<CTargetGunshipCrash> m_hCrashTarget; + float m_flNextGunshipCrashFind; + + CHandle<CCitadelEnergyCore> m_hEnergyCore; + + CNetworkVector( m_vecHitPos ); + + // If true, playing patrol loop. + // Else, playing angry. + bool m_fPatrolLoopPlaying; +}; + +LINK_ENTITY_TO_CLASS( npc_combinegunship, CNPC_CombineGunship ); + +IMPLEMENT_SERVERCLASS_ST( CNPC_CombineGunship, DT_CombineGunship ) + SendPropVector(SENDINFO(m_vecHitPos), -1, SPROP_COORD), +END_SEND_TABLE() + +BEGIN_DATADESC( CNPC_CombineGunship ) + + DEFINE_ENTITYFUNC( FlyTouch ), + + DEFINE_FIELD( m_flNextGroundAttack,FIELD_TIME ), + DEFINE_FIELD( m_bIsGroundAttacking,FIELD_BOOLEAN ), + DEFINE_FIELD( m_bCanGroundAttack, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flGroundAttackTime,FIELD_TIME ), + DEFINE_FIELD( m_pRotorWashModel, FIELD_CLASSPTR ), + DEFINE_FIELD( m_pSmokeTrail, FIELD_EHANDLE ), + DEFINE_FIELD( m_hGroundAttackTarget, FIELD_EHANDLE ), + DEFINE_SOUNDPATCH( m_pAirExhaustSound ), + DEFINE_SOUNDPATCH( m_pAirBlastSound ), + DEFINE_SOUNDPATCH( m_pCannonSound ), + DEFINE_FIELD( m_vecAngAcceleration,FIELD_VECTOR ), + DEFINE_FIELD( m_flDeltaT, FIELD_FLOAT ), + DEFINE_FIELD( m_flTimeNextAttack, FIELD_TIME ), + DEFINE_FIELD( m_flNextSeeEnemySound, FIELD_TIME ), + DEFINE_FIELD( m_flEndDestructTime, FIELD_TIME ), + DEFINE_FIELD( m_flNextRocket, FIELD_TIME ), + DEFINE_FIELD( m_iDoSmokePuff, FIELD_INTEGER ), + DEFINE_FIELD( m_iAmmoType, FIELD_INTEGER ), + DEFINE_FIELD( m_iBurstSize, FIELD_INTEGER ), + DEFINE_FIELD( m_flBurstDelay, FIELD_FLOAT ), + DEFINE_FIELD( m_fBlindfire, FIELD_BOOLEAN ), + DEFINE_FIELD( m_fOmniscient, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bIsFiring, FIELD_BOOLEAN ), + DEFINE_FIELD( m_iBurstHits, FIELD_INTEGER ), + DEFINE_FIELD( m_flTimeNextPing, FIELD_TIME ), + DEFINE_FIELD( m_flPenetrationDepth,FIELD_FLOAT ), + DEFINE_FIELD( m_vecAttackPosition, FIELD_VECTOR ), + DEFINE_FIELD( m_vecAttackVelocity, FIELD_VECTOR ), + DEFINE_FIELD( m_angGun, FIELD_VECTOR ), + DEFINE_PHYSPTR( m_pCrashingController ), + DEFINE_EMBEDDED( m_crashCallback ), + DEFINE_FIELD( m_hRagdoll, FIELD_EHANDLE ), + DEFINE_FIELD( m_hCrashTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_vecHitPos, FIELD_VECTOR ), + DEFINE_FIELD( m_fPatrolLoopPlaying,FIELD_BOOLEAN ), + DEFINE_FIELD( m_bPreFire, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bInvulnerable, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flNextGunshipCrashFind, FIELD_TIME ), + + DEFINE_FIELD( m_hEnergyCore, FIELD_EHANDLE ), + + DEFINE_ARRAY( m_bDamageOutputsFired, FIELD_BOOLEAN, GUNSHIP_NUM_DAMAGE_OUTPUTS ), + + // Function pointers + DEFINE_INPUTFUNC( FIELD_VOID, "OmniscientOn", InputOmniscientOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "OmniscientOff", InputOmniscientOff ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetPenetrationDepth", InputSetPenetrationDepth ), + DEFINE_INPUTFUNC( FIELD_VOID, "BlindfireOn", InputBlindfireOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "BlindfireOff", InputBlindfireOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "SelfDestruct", InputSelfDestruct ), + DEFINE_INPUTFUNC( FIELD_VOID, "SetDockingBBox", InputSetDockingBBox ), + DEFINE_INPUTFUNC( FIELD_VOID, "SetNormalBBox", InputSetNormalBBox ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableGroundAttack", InputEnableGroundAttack ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableGroundAttack", InputDisableGroundAttack ), + DEFINE_INPUTFUNC( FIELD_STRING, "DoGroundAttack", InputDoGroundAttack ), + + DEFINE_OUTPUT( m_OnFireCannon, "OnFireCannon" ), + DEFINE_OUTPUT( m_OnFirstDamage, "OnFirstDamage" ), + DEFINE_OUTPUT( m_OnSecondDamage, "OnSecondDamage" ), + DEFINE_OUTPUT( m_OnThirdDamage, "OnThirdDamage" ), + DEFINE_OUTPUT( m_OnFourthDamage, "OnFourthDamage" ), + DEFINE_OUTPUT( m_OnCrashed, "OnCrashed" ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CNPC_CombineGunship::CNPC_CombineGunship( void ) +{ + m_hGroundAttackTarget = NULL; + m_pSmokeTrail = NULL; + m_iAmmoType = -1; + m_pCrashingController = NULL; + m_hRagdoll = NULL; + m_hCrashTarget = NULL; +} + + +void CNPC_CombineGunship::CreateBellyBlastEnergyCore( void ) +{ + CCitadelEnergyCore *pCore = static_cast<CCitadelEnergyCore*>( CreateEntityByName( "env_citadel_energy_core" ) ); + + if ( pCore == NULL ) + return; + + m_hEnergyCore = pCore; + + int iAttachment = LookupAttachment( "BellyGun" ); + + Vector vOrigin; + QAngle vAngle; + + GetAttachment( iAttachment, vOrigin, vAngle ); + + pCore->SetAbsOrigin( vOrigin ); + pCore->SetAbsAngles( vAngle ); + + DispatchSpawn( pCore ); + pCore->Activate(); + + pCore->SetParent( this, iAttachment ); + pCore->SetScale( 4.0f ); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::Spawn( void ) +{ + Precache( ); + + if ( HasSpawnFlags( SF_GUNSHIP_USE_CHOPPER_MODEL ) ) + { + SetModel( "models/combine_helicopter.mdl" ); + } + else + { + SetModel( "models/gunship.mdl" ); + } + + ExtractBbox( SelectHeaviestSequence( ACT_GUNSHIP_PATROL ), m_cullBoxMins, m_cullBoxMaxs ); + BaseClass::Spawn(); + + InitPathingData( GUNSHIP_ARRIVE_DIST, GUNSHIP_MIN_CHASE_DIST_DIFF, sk_gunship_burst_min.GetFloat() ); + AddEFlags( EFL_NO_DISSOLVE | EFL_NO_MEGAPHYSCANNON_RAGDOLL | EFL_NO_PHYSCANNON_INTERACTION ); + + m_takedamage = DAMAGE_YES; + + SetHullType(HULL_LARGE_CENTERED); + SetHullSizeNormal(); + + m_iMaxHealth = m_iHealth = 100; + + m_flFieldOfView = -0.707; // 270 degrees + + m_fHelicopterFlags |= BITS_HELICOPTER_GUN_ON; + + InitBoneControllers(); + + InitCustomSchedules(); + + SetActivity( (Activity)ACT_GUNSHIP_PATROL ); + SetCollisionGroup( HL2COLLISION_GROUP_GUNSHIP ); + + m_flMaxSpeed = GUNSHIP_MAX_SPEED; + m_flMaxSpeedFiring = GUNSHIP_MAX_SPEED; + + m_flTimeNextAttack = gpGlobals->curtime; + m_flNextSeeEnemySound = gpGlobals->curtime; + + // Init the pose parameters + SetPoseParameter( "flex_horz", 0 ); + SetPoseParameter( "flex_vert", 0 ); + SetPoseParameter( "fin_accel", 0 ); + SetPoseParameter( "fin_sway", 0 ); + + if( m_iAmmoType == -1 ) + { + // Since there's no weapon to index the ammo type, + // do it manually here. + m_iAmmoType = GetAmmoDef()->Index("CombineCannon"); + } + + //!!!HACKHACK + // This tricks the AI code that constantly complains that the gunship has no schedule. + SetSchedule( SCHED_IDLE_STAND ); + + AddRelationship( "env_flare D_LI 9", NULL ); + AddRelationship( "rpg_missile D_HT 99", NULL ); + + m_flTimeNextPing = gpGlobals->curtime + 2; + + m_flPenetrationDepth = 24; + m_flBurstDelay = 2.0f; + + // Blindfire and Omniscience default to off + m_fBlindfire = false; + m_fOmniscient = false; + m_bIsFiring = false; + m_bPreFire = false; + m_bInvulnerable = false; + + // See if we should start being able to attack + m_bCanGroundAttack = ( m_spawnflags & SF_GUNSHIP_NO_GROUND_ATTACK ) ? false : true; + + m_flEndDestructTime = 0; + + m_iBurstSize = 0; + m_iBurstHits = 0; + + // Do not dissolve + AddEFlags( EFL_NO_DISSOLVE ); + + for ( int i = 0; i < GUNSHIP_NUM_DAMAGE_OUTPUTS; i++ ) + { + m_bDamageOutputsFired[i] = false; + } + + CapabilitiesAdd( bits_CAP_SQUAD); + + if ( hl2_episodic.GetBool() == true ) + { + CreateBellyBlastEnergyCore(); + } + + // Allows autoaim to help attack the gunship. + if( g_pGameRules->GetAutoAimMode() == AUTOAIM_ON_CONSOLE ) + { + AddFlag( FL_AIMTARGET ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Restore the motion controller +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::OnRestore( void ) +{ + BaseClass::OnRestore(); + + if ( m_pCrashingController ) + { + m_pCrashingController->SetEventHandler( &m_crashCallback ); + } +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::Precache( void ) +{ + if ( HasSpawnFlags( SF_GUNSHIP_USE_CHOPPER_MODEL ) ) + { + PrecacheModel( "models/combine_helicopter.mdl" ); + Chopper_PrecacheChunks( this ); + } + else + { + PrecacheModel("models/gunship.mdl"); + } + + PrecacheModel("sprites/lgtning.vmt"); + + PrecacheMaterial( "effects/ar2ground2" ); + PrecacheMaterial( "effects/blueblackflash" ); + + PrecacheScriptSound( "NPC_CombineGunship.SearchPing" ); + PrecacheScriptSound( "NPC_CombineGunship.PatrolPing" ); + PrecacheScriptSound( "NPC_Strider.Charge" ); + PrecacheScriptSound( "NPC_Strider.Shoot" ); + PrecacheScriptSound( "NPC_CombineGunship.SeeEnemy" ); + PrecacheScriptSound( "NPC_CombineGunship.CannonStartSound" ); + PrecacheScriptSound( "NPC_CombineGunship.Explode"); + PrecacheScriptSound( "NPC_CombineGunship.Pain" ); + PrecacheScriptSound( "NPC_CombineGunship.CannonStopSound" ); + + PrecacheScriptSound( "NPC_CombineGunship.DyingSound" ); + PrecacheScriptSound( "NPC_CombineGunship.CannonSound" ); + PrecacheScriptSound( "NPC_CombineGunship.RotorSound" ); + PrecacheScriptSound( "NPC_CombineGunship.ExhaustSound" ); + PrecacheScriptSound( "NPC_CombineGunship.RotorBlastSound" ); + + if ( hl2_episodic.GetBool() == true ) + { + UTIL_PrecacheOther( "env_citadel_energy_core" ); + g_iGunshipEffectIndex = PrecacheModel( "sprites/physbeam.vmt" ); + } + + PropBreakablePrecacheAll( MAKE_STRING("models/gunship.mdl") ); + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: Cache whatever pose parameters we intend to use +//----------------------------------------------------------------------------- +bool CNPC_CombineGunship::m_sbStaticPoseParamsLoaded = false; +int CNPC_CombineGunship::m_poseFlex_Horz = 0; +int CNPC_CombineGunship::m_poseFlex_Vert = 0; +int CNPC_CombineGunship::m_posePitch = 0; +int CNPC_CombineGunship::m_poseYaw = 0; +int CNPC_CombineGunship::m_poseFin_Accel = 0; +int CNPC_CombineGunship::m_poseFin_Sway = 0; +int CNPC_CombineGunship::m_poseWeapon_Pitch = 0; +int CNPC_CombineGunship::m_poseWeapon_Yaw = 0; +void CNPC_CombineGunship::PopulatePoseParameters( void ) +{ + if (!m_sbStaticPoseParamsLoaded) + { + m_poseFlex_Horz = LookupPoseParameter( "flex_horz"); + m_poseFlex_Vert = LookupPoseParameter( "flex_vert" ); + m_posePitch = LookupPoseParameter( "pitch" ); + m_poseYaw = LookupPoseParameter( "yaw" ); + m_poseFin_Accel = LookupPoseParameter( "fin_accel" ); + m_poseFin_Sway = LookupPoseParameter( "fin_sway" ); + + m_poseWeapon_Pitch = LookupPoseParameter( "weapon_pitch" ); + m_poseWeapon_Yaw = LookupPoseParameter( "weapon_yaw" ); + + m_sbStaticPoseParamsLoaded = true; + } + + BaseClass::PopulatePoseParameters(); +} + + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +CNPC_CombineGunship::~CNPC_CombineGunship(void) +{ + StopLoopingSounds(); + + if ( m_pCrashingController ) + { + physenv->DestroyMotionController( m_pCrashingController ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::Ping( void ) +{ + if( IsCrashing() ) + return; + + if( GetEnemy() != NULL ) + { + if( !HasCondition(COND_SEE_ENEMY) && gpGlobals->curtime > m_flTimeNextPing ) + { + EmitSound( "NPC_CombineGunship.SearchPing" ); + m_flTimeNextPing = gpGlobals->curtime + 3; + } + } + else + { + if( gpGlobals->curtime > m_flTimeNextPing ) + { + EmitSound( "NPC_CombineGunship.PatrolPing" ); + m_flTimeNextPing = gpGlobals->curtime + 3; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &pos - +// Output : float +//----------------------------------------------------------------------------- +float CNPC_CombineGunship::GroundDistToPosition( const Vector &pos ) +{ + Vector vecDiff; + VectorSubtract( GetAbsOrigin(), pos, vecDiff ); + + // Only interested in the 2d dist + vecDiff.z = 0; + + return vecDiff.Length(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::PlayPatrolLoop( void ) +{ + m_fPatrolLoopPlaying = true; + /* + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + controller.SoundChangeVolume( m_pPatrolSound, 1.0, 1.0 ); + controller.SoundChangeVolume( m_pAngrySound, 0.0, 1.0 ); + */ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::PlayAngryLoop( void ) +{ + m_fPatrolLoopPlaying = false; + /* + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + controller.SoundChangeVolume( m_pPatrolSound, 0.0, 1.0 ); + controller.SoundChangeVolume( m_pAngrySound, 1.0, 1.0 ); + */ +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::HelicopterPostThink( void ) +{ + // After HelicopterThink() + if ( HasCondition( COND_ENEMY_DEAD ) ) + { + if ( m_bIsFiring ) + { + // Fire more shots at the dead body for effect + if ( m_iBurstSize > 8 ) + { + m_iBurstSize = 8; + } + } + + // Fade out search sound, fade in patrol sound. + PlayPatrolLoop(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Vector +//----------------------------------------------------------------------------- +Vector CNPC_CombineGunship::GetGroundAttackHitPosition( void ) +{ + trace_t tr; + Vector vecShootPos, vecShootDir; + + GetAttachment( "BellyGun", vecShootPos, &vecShootDir, NULL, NULL ); + + AI_TraceLine( vecShootPos, vecShootPos + Vector( 0, 0, -MAX_TRACE_LENGTH ), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + if ( m_hGroundAttackTarget ) + { + return Vector( tr.endpos.x, tr.endpos.y, m_hGroundAttackTarget->WorldSpaceCenter().z ); + } + return tr.endpos; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_CombineGunship::CheckGroundAttack( void ) +{ + if ( m_bCanGroundAttack == false ) + return false; + + if ( m_bIsGroundAttacking ) + return false; + + // Must have an enemy + if ( GetEnemy() == NULL ) + return false; + + // Must not have done it too recently + if ( m_flNextGroundAttack > gpGlobals->curtime ) + return false; + + Vector predPos, predDest; + + // Find where the enemy is most likely to be in two seconds + UTIL_PredictedPosition( GetEnemy(), 1.0f, &predPos ); + UTIL_PredictedPosition( this, 1.0f, &predDest ); + + Vector predGap = ( predDest - predPos ); + predGap.z = 0; + + float predDistance = predGap.Length(); + + // Must be within distance + if ( predDistance > MIN_GROUND_ATTACK_DIST ) + return false; + + // Can't ground attack missiles + if ( IsTargettingMissile() ) + return false; + + //FIXME: Check to make sure we're not firing too far above or below the target + if ( fabs( GetGroundAttackHitPosition().z - GetEnemy()->WorldSpaceCenter().z ) > MIN_GROUND_ATTACK_HEIGHT_DIFF ) + return false; + + //FIXME: Check for ground movement capabilities? + + //TODO: Check for friendly-fire + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::StartGroundAttack( void ) +{ + // Mark us as attacking + m_bIsGroundAttacking = true; + m_flGroundAttackTime = gpGlobals->curtime + 3.0f; + + // Setup the attack effects + Vector vecShootPos; + + GetAttachment( "BellyGun", vecShootPos ); + + EntityMessageBegin( this, true ); + WRITE_BYTE( GUNSHIP_MSG_STREAKS ); + WRITE_VEC3COORD( vecShootPos ); + MessageEnd(); + + CPASAttenuationFilter filter2( this, "NPC_Strider.Charge" ); + EmitSound( filter2, entindex(), "NPC_Strider.Charge" ); + + Vector endpos = GetGroundAttackHitPosition(); + + CSoundEnt::InsertSound ( SOUND_DANGER, endpos, 1024, 0.5f ); + + if ( hl2_episodic.GetBool() == true ) + { + if ( m_hEnergyCore ) + { + variant_t value; + value.SetFloat( 3.0f ); + + g_EventQueue.AddEvent( m_hEnergyCore, "StartCharge", value, 0, this, this ); + } + } +} + +#define GUNSHIP_BELLY_BLAST_RADIUS 256.0f +#define BELLY_BLAST_MAX_PUNCH 5 + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::ManageWarningBeam( void ) +{ + Vector vecSrc, vecShootDir; + GetAttachment( "BellyGun", vecSrc, NULL, NULL, NULL ); + + trace_t tr; + CTraceFilterSkipTwoEntities filter( m_hGroundAttackTarget, this, COLLISION_GROUP_NONE ); + + UTIL_TraceLine( vecSrc, m_vecHitPos, MASK_SOLID, &filter, &tr ); + + int iPunch = 0; + + while ( tr.endpos != m_vecHitPos ) + { + iPunch++; + + if ( iPunch > BELLY_BLAST_MAX_PUNCH ) + break; + + if ( tr.fraction != 1.0 ) + { + if ( tr.m_pEnt ) + { + CTakeDamageInfo info( this, this, 1.0f, DMG_ENERGYBEAM ); + + Vector vTargetDir = tr.m_pEnt->BodyTarget( tr.endpos, false ) - tr.endpos; + + VectorNormalize( vTargetDir ); + + info.SetDamagePosition( tr.endpos + ( tr.plane.normal * 64.0f ) ); + info.SetDamageForce( vTargetDir * 100 ); + + if ( tr.m_pEnt->m_takedamage != DAMAGE_NO ) + { + // Deal damage + tr.m_pEnt->TakeDamage( info ); + } + } + + Vector vDir = m_vecHitPos - vecSrc; + VectorNormalize( vDir ); + + Vector vStartPunch = tr.endpos + vDir * 1; + + UTIL_TraceLine( vStartPunch, m_vecHitPos, MASK_SOLID, &filter, &tr ); + + if ( tr.startsolid ) + { + float flLength = (vStartPunch - tr.endpos).Length(); + + Vector vEndPunch = vStartPunch + vDir * ( flLength * tr.fractionleftsolid ); + + UTIL_TraceLine( vEndPunch, m_vecHitPos, MASK_SOLID, &filter, &tr ); + + trace_t tr2; + UTIL_TraceLine( vEndPunch, vEndPunch - vDir * 2, MASK_SOLID, &filter, &tr2 ); + + if ( (m_flGroundAttackTime - gpGlobals->curtime) <= 2.0f ) + { + g_pEffects->EnergySplash( tr2.endpos + vDir * 8, tr2.plane.normal, true ); + } + + g_pEffects->Sparks( tr2.endpos, 3.0f - (m_flGroundAttackTime-gpGlobals->curtime), 3.5f - (m_flGroundAttackTime-gpGlobals->curtime), &tr2.plane.normal ); + + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::DoBellyBlastDamage( trace_t &tr, Vector vMins, Vector vMaxs ) +{ + CBaseEntity* pList[100]; + + if ( g_debug_gunship.GetInt() == GUNSHIP_DEBUG_BELLYBLAST ) + { + NDebugOverlay::Box( tr.endpos, vMins, vMaxs, 255, 255, 0, true, 5.0f ); + } + + int count = UTIL_EntitiesInBox( pList, 100, tr.endpos + vMins, tr.endpos + vMaxs, 0 ); + + for ( int i = 0; i < count; i++ ) + { + CBaseEntity *pEntity = pList[i]; + + if ( pEntity == this ) + continue; + + if ( pEntity->m_takedamage == DAMAGE_NO ) + continue; + + float damage = 150; + + if ( pEntity->IsPlayer() ) + { + float damageDist = ( pEntity->GetAbsOrigin() - tr.endpos ).Length(); + damage = RemapValClamped( damageDist, 0, 300, 200, 0 ); + } + + CTakeDamageInfo info( this, this, damage, DMG_DISSOLVE ); + + Vector vTargetDir = pEntity->BodyTarget( tr.endpos, false ) - tr.endpos; + + VectorNormalize( vTargetDir ); + + info.SetDamagePosition( tr.endpos + ( tr.plane.normal * 64.0f ) ); + info.SetDamageForce( vTargetDir * 25000 ); + + // Deal damage + pEntity->TakeDamage( info ); + + trace_t groundTrace; + UTIL_TraceLine( pEntity->GetAbsOrigin(), pEntity->GetAbsOrigin() - Vector( 0, 0, 256 ), MASK_SOLID, pEntity, COLLISION_GROUP_NONE, &groundTrace ); + + if ( tr.fraction < 1.0f ) + { + CEffectData data; + + // Find the floor and add a dissolve explosion at that point + data.m_flRadius = GUNSHIP_BELLY_BLAST_RADIUS * 0.5f; + data.m_vNormal = groundTrace.plane.normal; + data.m_vOrigin = groundTrace.endpos; + + DispatchEffect( "AR2Explosion", data ); + } + + // If the creature was killed, then dissolve it + if ( pEntity->GetHealth() <= 0.0f ) + { + if ( pEntity->GetBaseAnimating() != NULL && !pEntity->IsEFlagSet( EFL_NO_DISSOLVE ) ) + { + pEntity->GetBaseAnimating()->Dissolve( NULL, gpGlobals->curtime ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::DoGroundAttackExplosion( void ) +{ + // Fire the bullets + Vector vecSrc, vecShootDir; + Vector vecAttachmentOrigin; + GetAttachment( "BellyGun", vecAttachmentOrigin, &vecShootDir, NULL, NULL ); + + vecSrc = vecAttachmentOrigin; + + if ( m_hGroundAttackTarget ) + { + vecSrc = m_hGroundAttackTarget->GetAbsOrigin(); + } + + Vector impactPoint = vecSrc + ( Vector( 0, 0, -1 ) * MAX_TRACE_LENGTH ); + + trace_t tr; + UTIL_TraceLine( vecSrc, impactPoint, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + UTIL_DecalTrace( &tr, "Scorch" ); + + if ( hl2_episodic.GetBool() == true ) + { + g_pEffects->EnergySplash( tr.endpos, tr.plane.normal ); + + CBroadcastRecipientFilter filter; + te->BeamRingPoint( filter, 0.0, + tr.endpos, //origin + 0, //start radius + GUNSHIP_BELLY_BLAST_RADIUS, //end radius + g_iGunshipEffectIndex, //texture + 0, //halo index + 0, //start frame + 0, //framerate + 0.2, //life + 10, //width + 0, //spread + 0, //amplitude + 255, //r + 255, //g + 255, //b + 50, //a + 0, //speed + FBEAM_FADEOUT + ); + } + + // Send the effect over + CEffectData data; + + // Do an extra effect if we struck the world + if ( tr.m_pEnt && tr.m_pEnt->IsWorld() ) + { + data.m_flRadius = GUNSHIP_BELLY_BLAST_RADIUS; + data.m_vNormal = tr.plane.normal; + data.m_vOrigin = tr.endpos; + + DispatchEffect( "AR2Explosion", data ); + } + + float flZLength = vecAttachmentOrigin.z - tr.endpos.z; + + Vector vBeamMins = Vector( -16, -16, 0 ); + Vector vBeamMaxs = Vector( 16, 16, flZLength ); + + DoBellyBlastDamage( tr, vBeamMins, vBeamMaxs ); + + Vector vBlastMins = Vector( -GUNSHIP_BELLY_BLAST_RADIUS, -GUNSHIP_BELLY_BLAST_RADIUS, 0 ); + Vector vBlastMaxs = Vector( GUNSHIP_BELLY_BLAST_RADIUS, GUNSHIP_BELLY_BLAST_RADIUS, 96 ); + + DoBellyBlastDamage( tr, vBlastMins, vBlastMaxs ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::StopGroundAttack( bool bDoAttack ) +{ + if ( !m_bIsGroundAttacking ) + return; + + // Mark us as no longer attacking + m_bIsGroundAttacking = false; + m_flNextGroundAttack = gpGlobals->curtime + 4.0f; + m_flTimeNextAttack = gpGlobals->curtime + 2.0f; + + Vector hitPos = GetGroundAttackHitPosition(); + + // tell the client side effect to complete + EntityMessageBegin( this, true ); + WRITE_BYTE( GUNSHIP_MSG_BIG_SHOT ); + WRITE_VEC3COORD( hitPos ); + MessageEnd(); + + if ( hl2_episodic.GetBool() == true ) + { + if ( m_hEnergyCore ) + { + variant_t value; + value.SetFloat( 1.0f ); + + g_EventQueue.AddEvent( m_hEnergyCore, "Stop", value, 0, this, this ); + } + } + + // Only attack if told to + if ( bDoAttack ) + { + CPASAttenuationFilter filter2( this, "NPC_Strider.Shoot" ); + EmitSound( filter2, entindex(), "NPC_Strider.Shoot"); + + ApplyAbsVelocityImpulse( Vector( 0, 0, 200.0f ) ); + + //ExplosionCreate( hitPos, QAngle( 0, 0, 1 ), this, 500, 500, true ); + DoGroundAttackExplosion(); + } + + // If we were attacking a target, revert to our previous target + if ( m_hGroundAttackTarget ) + { + m_hGroundAttackTarget = NULL; + if ( GetDestPathTarget() ) + { + // Return to our old path + SetupNewCurrentTarget( GetDestPathTarget() ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::DrawRotorWash( float flAltitude, const Vector &vecRotorOrigin ) +{ + // If we have a ragdoll, we want the wash under that, not me + if ( m_hRagdoll ) + { + BaseClass::DrawRotorWash( flAltitude, m_hRagdoll->GetAbsOrigin() ); + return; + } + + BaseClass::DrawRotorWash( flAltitude, vecRotorOrigin ); +} + +//------------------------------------------------------------------------------ +// Purpose : Override the desired position if your derived helicopter is doing something special +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::UpdateDesiredPosition( void ) +{ + if ( m_hCrashTarget ) + { + SetDesiredPosition( m_hCrashTarget->WorldSpaceCenter() + Vector(0,0,128) ); + } + else if ( m_hGroundAttackTarget ) + { + SetDesiredPosition( m_hGroundAttackTarget->GetAbsOrigin() + Vector(0,0,GUNSHIP_BELLYBLAST_TARGET_HEIGHT) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: do all of the stuff related to having an enemy, attacking, etc. +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::DoCombat( void ) +{ + // Check for enemy change-overs + if ( HasEnemy() ) + { + if ( HasCondition( COND_NEW_ENEMY ) ) + { + if ( GetEnemy() && GetEnemy()->IsPlayer() && m_flNextSeeEnemySound < gpGlobals->curtime ) + { + m_flNextSeeEnemySound = gpGlobals->curtime + 5.0; + + if ( !HasSpawnFlags( SF_GUNSHIP_USE_CHOPPER_MODEL ) ) + { + EmitSound( "NPC_CombineGunship.SeeEnemy" ); + } + } + + // If we're shooting at a missile, do it immediately! + if ( IsTargettingMissile() ) + { + EmitSound( "NPC_CombineGunship.SeeMissile" ); + + // Allow the gunship to attack again immediately + if ( ( m_flTimeNextAttack > gpGlobals->curtime ) && ( ( m_flTimeNextAttack - gpGlobals->curtime ) > GUNSHIP_MISSILE_MAX_RESPONSE_TIME ) ) + { + m_flTimeNextAttack = gpGlobals->curtime + GUNSHIP_MISSILE_MAX_RESPONSE_TIME; + m_iBurstSize = sk_gunship_burst_size.GetInt(); + } + } + + // Fade in angry sound, fade out patrol sound. + PlayAngryLoop(); + } + } + + // Do we have a belly blast target? + if ( m_hGroundAttackTarget && !m_bIsGroundAttacking ) + { + // If we're over it, blast. Can't use GetDesiredPosition() because it's not updated yet. + Vector vecTarget = m_hGroundAttackTarget->GetAbsOrigin() + Vector(0,0,GUNSHIP_BELLYBLAST_TARGET_HEIGHT); + Vector vecToTarget = (vecTarget - GetAbsOrigin()); + float flDistance = vecToTarget.Length(); + + // Get the difference between our velocity & the target's velocity + Vector vec2DVelocity = GetAbsVelocity(); + Vector vec2DTargetVelocity = m_hGroundAttackTarget->GetAbsVelocity(); + vec2DVelocity.z = vec2DTargetVelocity.z = 0; + float flVelocityDiff = (vec2DVelocity - vec2DTargetVelocity).Length(); + if ( flDistance < 100 && flVelocityDiff < 200 ) + { + StartGroundAttack(); + } + } + + // Update our firing + if ( m_bIsFiring ) + { + // Fire if we have rounds remaining in this burst + if ( ( m_iBurstSize > 0 ) && ( gpGlobals->curtime > m_flTimeNextAttack ) ) + { + UpdateEnemyTarget(); + FireCannonRound(); + } + else if ( m_iBurstSize < 1 ) + { + // We're done firing + StopCannonBurst(); + + if ( IsTargettingMissile() ) + { + m_flTimeNextAttack = gpGlobals->curtime + 0.5f; + } + } + } + else + { + // If we're not firing, look at the enemy + if ( GetEnemy() ) + { + m_vecAttackPosition = GetEnemy()->EyePosition(); + } + +#ifdef BELLYBLAST + // Check for a ground attack + if ( CheckGroundAttack() ) + { + StartGroundAttack(); + } +#endif + + // See if we're attacking + if ( m_bIsGroundAttacking ) + { + m_vecHitPos = GetGroundAttackHitPosition(); + + ManageWarningBeam(); + + // If our time is up, fire the blast and be done + if ( m_flGroundAttackTime < gpGlobals->curtime ) + { + // Fire! + StopGroundAttack( true ); + } + } + } + + // If we're using the chopper model, align the gun towards the target + if ( HasSpawnFlags( SF_GUNSHIP_USE_CHOPPER_MODEL ) ) + { + Vector vGunPosition; + GetAttachment( "gun", vGunPosition ); + Vector vecToAttackPos = (m_vecAttackPosition - vGunPosition); + PoseGunTowardTargetDirection( vecToAttackPos ); + } + + // Forget flares once I've seen them for a while + float flDeltaSeen = m_flLastSeen - m_flPrevSeen; + if ( GetEnemy() != NULL && GetEnemy()->Classify() == CLASS_FLARE && flDeltaSeen > GUNSHIP_FLARE_IGNORE_TIME ) + { + AddEntityRelationship( GetEnemy(), D_NU, 5 ); + + PlayPatrolLoop(); + + // Forget the flare now. + SetEnemy( NULL ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_CombineGunship::ChooseEnemy( void ) +{ + // If we're firing, don't switch enemies. This stops the gunship occasionally + // stopping a burst before he's really fired at all, which makes him look indecisive. + if ( m_bIsFiring ) + return true; + + return BaseClass::ChooseEnemy(); +} + +//----------------------------------------------------------------------------- +// Purpose: There's a lot of code in here now. We should consider moving +// helicopters and such to scheduled AI. (sjb) +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::MoveHead( void ) +{ + float flYaw = GetPoseParameter( m_poseFlex_Horz ); + float flPitch = GetPoseParameter( m_poseFlex_Vert ); + +/* + This head-turning code will cause the head to POP when switching from looking at the enemy + to looking according to the flight model. I will fix this later. Right now I'm turning + the code over to Ken for some aiming fixups. (sjb) +*/ + + while( 1 ) + { + if ( GetEnemy() != NULL ) + { + Vector vecToEnemy, vecAimDir; + float flDot; + + Vector vTargetPos, vGunPosition; + Vector vecTargetOffset; + QAngle vGunAngles; + + GetAttachment( "muzzle", vGunPosition, vGunAngles ); + + vTargetPos = GetEnemyTarget(); + + VectorSubtract( vTargetPos, vGunPosition, vecToEnemy ); + VectorNormalize( vecToEnemy ); + + // get angles relative to body position + AngleVectors( GetAbsAngles(), &vecAimDir ); + flDot = DotProduct( vecAimDir, vecToEnemy ); + + // Look at Enemy!! + if ( flDot > 0.3f ) + { + float flDiff; + + float flDesiredYaw = VecToYaw(vTargetPos - vGunPosition); + flDiff = UTIL_AngleDiff( flDesiredYaw, vGunAngles.y ) * 0.90; + flYaw = UTIL_Approach( flYaw + flDiff, flYaw, 5.0 ); + + float flDesiredPitch = UTIL_VecToPitch(vTargetPos - vGunPosition); + flDiff = UTIL_AngleDiff( flDesiredPitch, vGunAngles.x ) * 0.90; + flPitch = UTIL_Approach( flPitch + flDiff, flPitch, 5.0 ); + + break; + } + } + + // Look where going! +#if 1 // old way- look according to rotational velocity + flYaw = UTIL_Approach( GetLocalAngularVelocity().y, flYaw, 2.0 * 10 * m_flDeltaT ); + flPitch = UTIL_Approach( GetLocalAngularVelocity().x, flPitch, 2.0 * 10 * m_flDeltaT ); +#else // new way- look towards the next waypoint? + // !!!UNDONE +#endif + break; + } + + // Set the body flexes + SetPoseParameter( m_poseFlex_Vert, flPitch ); + SetPoseParameter( m_poseFlex_Horz, flYaw ); +} + + +//----------------------------------------------------------------------------- +// Purpose: There's a lot of code in here now. We should consider moving +// helicopters and such to scheduled AI. (sjb) +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::PrescheduleThink( void ) +{ + m_flDeltaT = gpGlobals->curtime - GetLastThink(); + + // Are we crashing? + if ( m_flEndDestructTime && gpGlobals->curtime > m_flEndDestructTime ) + { + // We're dead, remove ourselves + SelfDestruct(); + return; + } + + if( m_lifeState == LIFE_ALIVE ) + { + // Chopper doesn't ping + if ( !HasSpawnFlags( SF_GUNSHIP_USE_CHOPPER_MODEL ) ) + { + Ping(); + } + + DoCombat(); + MoveHead(); + } + else if( m_lifeState == LIFE_DYING ) + { + // Increase the number of explosions as he gets closer to death + bool bCreateExplosion = false; + float flTimeLeft = m_flEndDestructTime - gpGlobals->curtime; + if ( flTimeLeft > 1.5 ) + { + bCreateExplosion = (random->RandomInt( 0, 3 ) == 0); + } + else + { + bCreateExplosion = (random->RandomInt( 0, 2 ) == 0); + } + + if ( bCreateExplosion ) + { + Vector explodePoint; + if ( m_hRagdoll ) + { + m_hRagdoll->CollisionProp()->RandomPointInBounds( Vector(0.25,0.25,0.25), Vector(0.75,0.75,0.75), &explodePoint ); + } + else + { + CollisionProp()->RandomPointInBounds( Vector(0.25,0.25,0.25), Vector(0.75,0.75,0.75), &explodePoint ); + + // Knock the gunship a little, but not if we're trying to fly to a point + if ( !m_hCrashTarget ) + { + Vector vecPush = (GetAbsOrigin() - explodePoint); + VectorNormalize( vecPush ); + ApplyAbsVelocityImpulse( vecPush * 128 ); + } + } + + ExplosionCreate( explodePoint, QAngle(0,0,1), this, 100, 128, false ); + } + + // Have we reached our crash point? + if ( m_flNextGunshipCrashFind && !m_hRagdoll ) + { + // Update nearest crash point. The RPG that killed us may have knocked us + // closer to a different point than the one we were near when we first died. + if ( m_flNextGunshipCrashFind < gpGlobals->curtime ) + { + FindNearestGunshipCrash(); + } + + if ( m_hCrashTarget ) + { + MoveHead(); + + UpdateDesiredPosition(); + + // If we're over it, destruct + Vector vecToTarget = (GetDesiredPosition() - GetAbsOrigin()); + if ( vecToTarget.LengthSqr() < (384 * 384) ) + { + BeginDestruct(); + m_OnCrashed.FireOutput( this, this ); + m_hCrashTarget->GunshipCrashedOnTarget(); + return; + } + } + } + } + + BaseClass::PrescheduleThink(); + +#ifdef JACOBS_GUNSHIP + SetPoseParameter( m_posePitch, random->RandomFloat( GUNSHIP_HEAD_MAX_LEFT, GUNSHIP_HEAD_MAX_RIGHT ) ); + SetPoseParameter( m_poseYaw, random->RandomFloat( GUNSHIP_HEAD_MAX_UP, GUNSHIP_HEAD_MAX_DOWN ) ); +#endif + +} + +//------------------------------------------------------------------------------ +// Purpose : If the enemy is in front of the gun, load up a burst. +// Actual gunfire is handled in PrescheduleThink +// Input : +// Output : +//------------------------------------------------------------------------------ +bool CNPC_CombineGunship::FireGun( void ) +{ + if ( m_lifeState != LIFE_ALIVE ) + return false; + + if ( m_bIsGroundAttacking ) + return false; + + if ( GetEnemy() && !m_bIsFiring && gpGlobals->curtime > m_flTimeNextAttack ) + { + // We want to decelerate to attack + if (m_flGoalSpeed > GetMaxSpeedFiring() ) + { + m_flGoalSpeed = GetMaxSpeedFiring(); + } + + bool bTargetingMissile = IsTargettingMissile(); + if ( !bTargetingMissile && !m_bPreFire ) + { + m_bPreFire = true; + m_flTimeNextAttack = gpGlobals->curtime + 0.5f; + + EmitSound( "NPC_CombineGunship.CannonStartSound" ); + return false; + } + + //TODO: Emit the danger noise and wait until it's finished + + // Don't fire at an occluded enemy unless blindfire is on. + if ( HasCondition( COND_ENEMY_OCCLUDED ) && ( m_fBlindfire == false ) ) + return false; + + // Don't shoot if the enemy is too close + if ( !bTargetingMissile && GroundDistToPosition( GetEnemy()->GetAbsOrigin() ) < GUNSHIP_STITCH_MIN ) + return false; + + Vector vecAimDir, vecToEnemy; + Vector vecMuzzle, vecEnemyTarget; + + GetAttachment( "muzzle", vecMuzzle, &vecAimDir, NULL, NULL ); + vecEnemyTarget = GetEnemyTarget(); + + // Aim with the muzzle's attachment point. + VectorSubtract( vecEnemyTarget, vecMuzzle, vecToEnemy ); + + VectorNormalize( vecToEnemy ); + VectorNormalize( vecAimDir ); + + if ( DotProduct( vecToEnemy, vecAimDir ) > 0.9 ) + { + StartCannonBurst( sk_gunship_burst_size.GetInt() ); + return true; + } + + return false; + } + + return false; +} + +//------------------------------------------------------------------------------ +// Purpose: Fire a round from the cannon +// Notes: Only call this if you have an enemy. +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::FireCannonRound( void ) +{ + Vector vecPenetrate; + trace_t tr; + + Vector vecToEnemy, vecEnemyTarget; + Vector vecMuzzle; + Vector vecAimDir; + + GetAttachment( "muzzle", vecMuzzle, &vecAimDir ); + vecEnemyTarget = GetEnemyTarget(); + + // Aim with the muzzle's attachment point. + VectorSubtract( vecEnemyTarget, vecMuzzle, vecToEnemy ); + VectorNormalize( vecToEnemy ); + + // If the gun is wildly off target, stop firing! + // FIXME - this should use a vector pointing + // to the enemy's location PLUS the stitching + // error! (sjb) !!!BUGBUG + + if ( g_debug_gunship.GetInt() == GUNSHIP_DEBUG_STITCHING ) + { + QAngle vecAimAngle; + Vector vForward, vRight, vUp; + GetAttachment( "muzzle", vecMuzzle, &vForward, &vRight, &vUp ); + AngleVectors( vecAimAngle, &vForward, &vRight, &vUp ); + NDebugOverlay::Line( vecMuzzle, vecEnemyTarget, 255, 255, 0, true, 1.0f ); + + NDebugOverlay::Line( vecMuzzle, vecMuzzle + ( vForward * 64.0f ), 255, 0, 0, true, 1.0f ); + NDebugOverlay::Line( vecMuzzle, vecMuzzle + ( vRight * 32.0f ), 0, 255, 0, true, 1.0f ); + NDebugOverlay::Line( vecMuzzle, vecMuzzle + ( vUp * 32.0f ), 0, 0, 255, true, 1.0f ); + } + + // Robin: Check the dotproduct to the enemy, NOT to the offsetted firing angle + // Fixes problems firing at close enemies, where the enemy is valid but + // the offset firing stitch isn't. + Vector vecDotCheck = vecToEnemy; + if ( GetEnemy() ) + { + VectorSubtract( GetEnemy()->GetAbsOrigin(), vecMuzzle, vecDotCheck ); + VectorNormalize( vecDotCheck ); + } + + if ( DotProduct( vecDotCheck, vecAimDir ) < 0.8f ) + { + StopCannonBurst(); + return; + } + + DoMuzzleFlash(); + + m_OnFireCannon.FireOutput( this, this, 0 ); + + m_flTimeNextAttack = gpGlobals->curtime + 0.05f; + + float flPrevHealth = 0; + if ( GetEnemy() ) + { + flPrevHealth = GetEnemy()->GetHealth(); + } + + // Make sure we hit missiles + if ( IsTargettingMissile() ) + { + // Fire a fake shot + FireBullets( 1, vecMuzzle, vecToEnemy, VECTOR_CONE_5DEGREES, 8192, m_iAmmoType, 1 ); + + CBaseEntity *pMissile = GetEnemy(); + + Vector missileDir, threatDir; + + AngleVectors( pMissile->GetAbsAngles(), &missileDir ); + + threatDir = ( WorldSpaceCenter() - pMissile->GetAbsOrigin() ); + float threatDist = VectorNormalize( threatDir ); + + // Check that the target is within some threshold + if ( ( DotProduct( threatDir, missileDir ) > 0.95f ) && ( threatDist < 1024.0f ) ) + { + if ( random->RandomInt( 0, 1 ) == 0 ) + { + CTakeDamageInfo info( this, this, 200, DMG_MISSILEDEFENSE ); + CalculateBulletDamageForce( &info, m_iAmmoType, -threatDir, WorldSpaceCenter() ); + GetEnemy()->TakeDamage( info ); + } + } + else + { + //FIXME: Some other metric + } + } + else + { + m_iBurstSize--; + + // Fire directly at the target + FireBulletsInfo_t info( 1, vecMuzzle, vecToEnemy, vec3_origin, MAX_COORD_RANGE, m_iAmmoType ); + info.m_iTracerFreq = 1; + CAmmoDef *pAmmoDef = GetAmmoDef(); + info.m_iPlayerDamage = pAmmoDef->PlrDamage( m_iAmmoType ); + + // If we've already hit the player, do 0 damage. This ensures we don't hit the + // player multiple times during a single burst. + if ( m_iBurstHits >= GUNSHIP_MAX_HITS_PER_BURST ) + { + info.m_iPlayerDamage = 1; + } + + FireBullets( info ); + + if ( GetEnemy() && flPrevHealth != GetEnemy()->GetHealth() ) + { + m_iBurstHits++; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::DoMuzzleFlash( void ) +{ + BaseClass::DoMuzzleFlash(); + + CEffectData data; + + data.m_nAttachmentIndex = LookupAttachment( "muzzle" ); + data.m_nEntIndex = entindex(); + DispatchEffect( "GunshipMuzzleFlash", data ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_CombineGunship::FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker ) +{ + bool fReturn = BaseClass::FVisible( pEntity, traceMask, ppBlocker ); + + if( m_fOmniscient ) + { + if( !fReturn ) + { + // Set this condition so that we can check it later and know that the + // enemy truly is occluded, but the gunship regards it as visible due + // to omniscience. + SetCondition( COND_ENEMY_OCCLUDED ); + } + else + { + ClearCondition( COND_ENEMY_OCCLUDED ); + } + + return true; + } + + if( fReturn ) + { + ClearCondition( COND_ENEMY_OCCLUDED ); + } + else + { + SetCondition( COND_ENEMY_OCCLUDED ); + } + + return fReturn; +} + + +//----------------------------------------------------------------------------- +// Purpose: Change the depth that gunship bullets can penetrate through solids +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::InputSetPenetrationDepth( inputdata_t &inputdata ) +{ + m_flPenetrationDepth = inputdata.value.Float(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Allow the gunship to sense its enemy's location even when enemy +// is hidden from sight. +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::InputOmniscientOn( inputdata_t &inputdata ) +{ + m_fOmniscient = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns the gunship to its default requirement that it see the +// enemy to know its current position +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::InputOmniscientOff( inputdata_t &inputdata ) +{ + m_fOmniscient = false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Allows the gunship to fire at an unseen enemy. The gunship is relying +// on hitting the target with bullets that will punch through the +// cover that the enemy is hiding behind. (Such as the Depot lighthouse) +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::InputBlindfireOn( inputdata_t &inputdata ) +{ + m_fBlindfire = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns the gunship to default rules for attacking the enemy. The +// enemy must be seen to be fired at. +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::InputBlindfireOff( inputdata_t &inputdata ) +{ + m_fBlindfire = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Set the gunship's paddles flailing! +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::Event_Killed( const CTakeDamageInfo &info ) +{ + m_takedamage = DAMAGE_NO; + + StopCannonBurst(); + + // Replace the rotor sound with broken engine sound. + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + controller.SoundDestroy( m_pRotorSound ); + + // BUGBUG: Isn't this sound just going to get stomped when the base class calls StopLoopingSounds() ?? + CPASAttenuationFilter filter2( this ); + m_pRotorSound = controller.SoundCreate( filter2, entindex(), "NPC_CombineGunship.DyingSound" ); + controller.Play( m_pRotorSound, 1.0, 100 ); + + m_OnDeath.FireOutput( info.GetAttacker(), this ); + SendOnKilledGameEvent( info ); + + BeginCrash(); + + // we deliberately do not call BaseClass::EventKilled +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::BeginCrash( void ) +{ + m_lifeState = LIFE_DYING; + StopGroundAttack( false ); + + // Increase our smoke trail + CreateSmokeTrail(); + if ( m_pSmokeTrail ) + { + m_pSmokeTrail->SetLifetime( -1 ); + m_pSmokeTrail->m_StartSize = 64; + m_pSmokeTrail->m_EndSize = 128; + m_pSmokeTrail->m_Opacity = 0.5f; + } + + if ( !FindNearestGunshipCrash() ) + { + // We couldn't find a crash target, so just die right here. + BeginDestruct(); + return; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_CombineGunship::FindNearestGunshipCrash( void ) +{ + // Find the nearest crash point. If we find one, we'll try to fly to it and die. + // If we can't find one, we'll die right here. + bool bFoundAnyCrashTargets = false; + float flNearest = MAX_TRACE_LENGTH * MAX_TRACE_LENGTH; + CTargetGunshipCrash *pNearest = NULL; + CBaseEntity *pEnt = NULL; + while( (pEnt = gEntList.FindEntityByClassname(pEnt, "info_target_gunshipcrash")) != NULL ) + { + CTargetGunshipCrash *pCrashTarget = assert_cast<CTargetGunshipCrash*>(pEnt); + if ( pCrashTarget->IsDisabled() ) + continue; + + bFoundAnyCrashTargets = true; + + float flDist = ( pEnt->WorldSpaceCenter() - WorldSpaceCenter() ).LengthSqr(); + if( flDist < flNearest ) + { + trace_t tr; + UTIL_TraceLine( WorldSpaceCenter(), pEnt->WorldSpaceCenter(), MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr ); + if( tr.fraction == 1.0 ) + { + pNearest = pCrashTarget; + flNearest = flDist; + } + else if ( g_debug_gunship.GetInt() ) + { + NDebugOverlay::Line( WorldSpaceCenter(), tr.endpos, 255,0,0, true, 99); + } + } + } + + if ( !pNearest ) + { + // If we found a gunship crash, but none near enough, claim we did find one, so that we + // don't blow up yet. This will give us 3 seconds to attempt to find one before dying. + if ( !m_hCrashTarget && bFoundAnyCrashTargets ) + { + m_flNextGunshipCrashFind = gpGlobals->curtime + 0.5; + m_flEndDestructTime = gpGlobals->curtime + 3.0; + return true; + } + + return false; + } + + // Fly to the crash point and destruct there + m_hCrashTarget = pNearest; + m_flNextGunshipCrashFind = gpGlobals->curtime + 0.5; + m_flEndDestructTime = 0; + + if ( g_debug_gunship.GetInt() ) + { + NDebugOverlay::Line(GetAbsOrigin(), m_hCrashTarget->GetAbsOrigin(), 0,255,0, true, 0.5); + NDebugOverlay::Box( m_hCrashTarget->GetAbsOrigin(), -Vector(200,200,200), Vector(200,200,200), 0,255,0, 128, 0.5 ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: I'm now ready to die. Create my ragdoll & hide myself. +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::BeginDestruct( void ) +{ + m_flEndDestructTime = gpGlobals->curtime + 3.0; + + // Clamp velocity + if( hl2_episodic.GetBool() && GetAbsVelocity().Length() > 700.0f ) + { + Vector vecVelocity = GetAbsVelocity(); + VectorNormalize( vecVelocity ); + SetAbsVelocity( vecVelocity * 700.0f ); + } + + CTakeDamageInfo info; + info.SetDamage( 40000 ); + CalculateExplosiveDamageForce( &info, GetAbsVelocity(), GetAbsOrigin() ); + + // Don't create a ragdoll if we're going to explode into gibs + if ( !m_hCrashTarget ) + return; + + // Switch to damaged skin + m_nSkin = 1; + + if ( HasSpawnFlags( SF_GUNSHIP_USE_CHOPPER_MODEL ) ) + { + Chopper_BecomeChunks( this ); + SetThink( &CNPC_CombineGunship::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); + AddEffects( EF_NODRAW ); + return; + } + + // Create the ragdoll + m_hRagdoll = CreateServerRagdoll( this, 0, info, COLLISION_GROUP_NONE ); + if ( !m_hRagdoll ) + { + // Failed, just explode + SelfDestruct(); + return; + } + + m_hRagdoll->SetName( AllocPooledString( UTIL_VarArgs("%s_ragdoll", STRING(GetEntityName()) ) ) ); + + // Tell the smoke trail to follow the ragdoll + CreateSmokeTrail(); + if ( m_pSmokeTrail ) + { + // Force the smoke trail to stay on, and tell it to follow the ragdoll + m_pSmokeTrail->SetLifetime( -1 ); + m_pSmokeTrail->FollowEntity( m_hRagdoll ); + + m_pSmokeTrail->m_StartSize = 64; + m_pSmokeTrail->m_EndSize = 128; + m_pSmokeTrail->m_Opacity = 0.5f; + } + + /* + // ROBIN: Disabled this for now. + // + // Create the crashing controller and attach it to the ragdoll physics objects + m_pCrashingController = physenv->CreateMotionController( &m_crashCallback ); + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = m_hRagdoll->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + for ( int i = 0; i < count; i++ ) + { + m_pCrashingController->AttachObject( pList[i], false ); + } + */ + + // Hide myself, because the ragdoll's now taken my place + AddEffects( EF_NODRAW ); + AddSolidFlags( FSOLID_NOT_SOLID ); +} + +//----------------------------------------------------------------------------- +// Purpose: Create a smoke trail +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::CreateSmokeTrail( void ) +{ + if ( m_pSmokeTrail ) + return; + + m_pSmokeTrail = SmokeTrail::CreateSmokeTrail(); + + if ( m_pSmokeTrail ) + { + m_pSmokeTrail->m_SpawnRate = 48; + m_pSmokeTrail->m_ParticleLifetime = 2.5f; + + m_pSmokeTrail->m_StartColor.Init( 0.25f, 0.25f, 0.25f ); + m_pSmokeTrail->m_EndColor.Init( 0.0, 0.0, 0.0 ); + + m_pSmokeTrail->m_StartSize = 24; + m_pSmokeTrail->m_EndSize = 128; + m_pSmokeTrail->m_SpawnRadius = 4; + m_pSmokeTrail->m_MinSpeed = 8; + m_pSmokeTrail->m_MaxSpeed = 64; + m_pSmokeTrail->m_Opacity = 0.2f; + + m_pSmokeTrail->SetLifetime( -1 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::ApplyGeneralDrag( void ) +{ + Vector vecNewVelocity = GetAbsVelocity(); + + // See if we need to stop more quickly + if ( m_bIsGroundAttacking ) + { + vecNewVelocity *= 0.95f; + } + else + { + vecNewVelocity *= 0.995; + } + + SetAbsVelocity( vecNewVelocity ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- + +void CNPC_CombineGunship::Flight( void ) +{ + if( GetFlags() & FL_ONGROUND ) + { + //This would be really bad. + SetGroundEntity( NULL ); + } + + if ( g_debug_gunship.GetInt() == GUNSHIP_DEBUG_PATH ) + { + NDebugOverlay::Line(GetLocalOrigin(), GetDesiredPosition(), 0,0,255, true, 0.1); + } + + // calc desired acceleration + float dt = 1.0f; + + Vector accel; + float accelRate = GUNSHIP_ACCEL_RATE; + float maxSpeed = GetMaxSpeed(); + + if ( m_lifeState == LIFE_DYING && m_hCrashTarget != NULL ) + { + // Gunship can fly faster to the place where it's supposed to crash, but + // maintain normal speeds if we haven't found a place to crash. + accelRate *= 2.0; + maxSpeed *= 4.0; + } + + float flCurrentSpeed = GetAbsVelocity().Length(); + float flDist = MIN( flCurrentSpeed + accelRate, maxSpeed ); + + Vector deltaPos; + if ( m_lifeState == LIFE_DYING || m_hGroundAttackTarget ) + { + // Move directly to the target point + deltaPos = GetDesiredPosition(); + } + else + { + ComputeActualTargetPosition( flDist, dt, 0.0f, &deltaPos ); + } + deltaPos -= GetAbsOrigin(); + + // calc goal linear accel to hit deltaPos in dt time. + accel.x = 2.0 * (deltaPos.x - GetAbsVelocity().x * dt) / (dt * dt); + accel.y = 2.0 * (deltaPos.y - GetAbsVelocity().y * dt) / (dt * dt); + accel.z = 2.0 * (deltaPos.z - GetAbsVelocity().z * dt + 0.5 * 384 * dt * dt) / (dt * dt); + + float flDistFromPath = 0.0f; + Vector vecPoint, vecDelta; + if ( m_lifeState != LIFE_DYING && IsOnPathTrack() ) + { + // Also, add in a little force to get us closer to our current line segment if we can + ClosestPointToCurrentPath( &vecPoint ); + VectorSubtract( vecPoint, GetAbsOrigin(), vecDelta ); + flDistFromPath = VectorNormalize( vecDelta ); + if ( flDistFromPath > GUNSHIP_OUTER_NAV_DIST ) + { + // Strongly constrain to an n unit pipe around the current path + // by damping out all impulse forces that would push us further from the pipe + float flAmount = (flDistFromPath - GUNSHIP_OUTER_NAV_DIST) / 200.0f; + flAmount = clamp( flAmount, 0, 1 ); + VectorMA( accel, flAmount * 200.0f, vecDelta, accel ); + } + } + + Vector vecAvoidForce; + CAvoidSphere::ComputeAvoidanceForces( this, 350.0f, 2.0f, &vecAvoidForce ); + accel += vecAvoidForce; + CAvoidBox::ComputeAvoidanceForces( this, 350.0f, 2.0f, &vecAvoidForce ); + accel += vecAvoidForce; + + if ( m_lifeState != LIFE_DYING || m_hCrashTarget == NULL ) + { + // don't fall faster than 0.2G or climb faster than 2G + accel.z = clamp( accel.z, 384 * 0.2, 384 * 2.0 ); + } + + Vector forward, right, up; + GetVectors( &forward, &right, &up ); + + Vector goalUp = accel; + VectorNormalize( goalUp ); + + // calc goal orientation to hit linear accel forces + float goalPitch = RAD2DEG( asin( DotProduct( forward, goalUp ) ) ); + float goalYaw = UTIL_VecToYaw( m_vecDesiredFaceDir ); + float goalRoll = RAD2DEG( asin( DotProduct( right, goalUp ) ) ); + + // clamp goal orientations + goalPitch = clamp( goalPitch, -45, 60 ); + goalRoll = clamp( goalRoll, -45, 45 ); + + // calc angular accel needed to hit goal pitch in dt time. + dt = 0.6; + QAngle goalAngAccel; + goalAngAccel.x = 2.0 * (AngleDiff( goalPitch, AngleNormalize( GetLocalAngles().x ) ) - GetLocalAngularVelocity().x * dt) / (dt * dt); + goalAngAccel.y = 2.0 * (AngleDiff( goalYaw, AngleNormalize( GetLocalAngles().y ) ) - GetLocalAngularVelocity().y * dt) / (dt * dt); + goalAngAccel.z = 2.0 * (AngleDiff( goalRoll, AngleNormalize( GetLocalAngles().z ) ) - GetLocalAngularVelocity().z * dt) / (dt * dt); + + goalAngAccel.x = clamp( goalAngAccel.x, -300, 300 ); + //goalAngAccel.y = clamp( goalAngAccel.y, -60, 60 ); + goalAngAccel.y = clamp( goalAngAccel.y, -120, 120 ); + goalAngAccel.z = clamp( goalAngAccel.z, -300, 300 ); + + // limit angular accel changes to similate mechanical response times + dt = 0.1; + QAngle angAccelAccel; + angAccelAccel.x = (goalAngAccel.x - m_vecAngAcceleration.x) / dt; + angAccelAccel.y = (goalAngAccel.y - m_vecAngAcceleration.y) / dt; + angAccelAccel.z = (goalAngAccel.z - m_vecAngAcceleration.z) / dt; + + angAccelAccel.x = clamp( angAccelAccel.x, -1000, 1000 ); + angAccelAccel.y = clamp( angAccelAccel.y, -1000, 1000 ); + angAccelAccel.z = clamp( angAccelAccel.z, -1000, 1000 ); + + m_vecAngAcceleration += angAccelAccel * 0.1; + + // DevMsg( "pitch %6.1f (%6.1f:%6.1f) ", goalPitch, GetLocalAngles().x, m_vecAngVelocity.x ); + // DevMsg( "roll %6.1f (%6.1f:%6.1f) : ", goalRoll, GetLocalAngles().z, m_vecAngVelocity.z ); + // DevMsg( "%6.1f %6.1f %6.1f : ", goalAngAccel.x, goalAngAccel.y, goalAngAccel.z ); + // DevMsg( "%6.0f %6.0f %6.0f\n", angAccelAccel.x, angAccelAccel.y, angAccelAccel.z ); + + ApplySidewaysDrag( right ); + ApplyGeneralDrag(); + + QAngle angVel = GetLocalAngularVelocity(); + angVel += m_vecAngAcceleration * 0.1; + + //angVel.y = clamp( angVel.y, -60, 60 ); + //angVel.y = clamp( angVel.y, -120, 120 ); + angVel.y = clamp( angVel.y, -120, 120 ); + + SetLocalAngularVelocity( angVel ); + + m_flForce = m_flForce * 0.8 + (accel.z + fabs( accel.x ) * 0.1 + fabs( accel.y ) * 0.1) * 0.1 * 0.2; + + Vector vecImpulse = m_flForce * up; + + if ( !m_hCrashTarget && m_lifeState == LIFE_DYING && !hl2_episodic.GetBool() ) + { + // Force gunship to the ground if it doesn't have a specific place to crash. + // EXCEPT In episodic, where forcing it to the ground means it crashes where the player can't see (attic showdown) (sjb) + vecImpulse.z = -10; + } + else + { + vecImpulse.z -= 38.4; // 32ft/sec + } + + // Find our current velocity + Vector vecVelDir = GetAbsVelocity(); + VectorNormalize( vecVelDir ); + + if ( flDistFromPath > GUNSHIP_INNER_NAV_DIST ) + { + // Strongly constrain to an n unit pipe around the current path + // by damping out all impulse forces that would push us further from the pipe + float flDot = DotProduct( vecImpulse, vecDelta ); + if ( flDot < 0.0f ) + { + VectorMA( vecImpulse, -flDot * 0.1f, vecDelta, vecImpulse ); + } + + // Also apply an extra impulse to compensate for the current velocity + flDot = DotProduct( vecVelDir, vecDelta ); + if ( flDot < 0.0f ) + { + VectorMA( vecImpulse, -flDot * 0.1f, vecDelta, vecImpulse ); + } + } + + // Find our acceleration direction + Vector vecAccelDir = vecImpulse; + VectorNormalize( vecAccelDir ); + + // Level out our plane of movement + vecAccelDir.z = 0.0f; + vecVelDir.z = 0.0f; + forward.z = 0.0f; + right.z = 0.0f; + + // Find out how "fast" we're moving in relation to facing and acceleration + float speed = m_flForce * DotProduct( vecVelDir, vecAccelDir );// * DotProduct( forward, vecVelDir ); + + // Apply the acceleration blend to the fins + float finAccelBlend = SimpleSplineRemapVal( speed, -60, 60, -1, 1 ); + float curFinAccel = GetPoseParameter( m_poseFin_Accel ); + + curFinAccel = UTIL_Approach( finAccelBlend, curFinAccel, 0.5f ); + SetPoseParameter( m_poseFin_Accel, curFinAccel ); + + speed = m_flForce * DotProduct( vecVelDir, right ); + + // Apply the spin sway to the fins + float finSwayBlend = SimpleSplineRemapVal( speed, -60, 60, -1, 1 ); + float curFinSway = GetPoseParameter( m_poseFin_Sway ); + + curFinSway = UTIL_Approach( finSwayBlend, curFinSway, 0.5f ); + SetPoseParameter( m_poseFin_Sway, curFinSway ); + + if ( g_debug_gunship.GetInt() == GUNSHIP_DEBUG_PATH ) + { + NDebugOverlay::Line(GetLocalOrigin(), GetLocalOrigin() + vecImpulse, 255,0,0, true, 0.1); + } + + // Add in our velocity pulse for this frame + ApplyAbsVelocityImpulse( vecImpulse ); +} + +//------------------------------------------------------------------------------ +// Updates the facing direction +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::UpdateFacingDirection( void ) +{ + if ( GetEnemy() ) + { + if ( !IsCrashing() && m_flLastSeen + 5 > gpGlobals->curtime ) + { + // If we've seen the target recently, face the target. + //Msg( "Facing Target \n" ); + m_vecDesiredFaceDir = m_vecTargetPosition - GetAbsOrigin(); + } + else + { + // Remain facing the way you were facing... + } + } + else + { + // Face our desired position. + if ( GetDesiredPosition().DistToSqr( GetAbsOrigin() ) > 1 ) + { + m_vecDesiredFaceDir = GetDesiredPosition() - GetAbsOrigin(); + } + else + { + GetVectors( &m_vecDesiredFaceDir, NULL, NULL ); + } + } + VectorNormalize( m_vecDesiredFaceDir ); +} + +//------------------------------------------------------------------------------ +// Purpose : Fire up the Gunships 'second' rotor sound. The Search sound. +// Input : +// Output : +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::InitializeRotorSound( void ) +{ + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + CPASAttenuationFilter filter( this ); + + m_pCannonSound = controller.SoundCreate( filter, entindex(), "NPC_CombineGunship.CannonSound" ); + m_pRotorSound = controller.SoundCreate( filter, entindex(), "NPC_CombineGunship.RotorSound" ); + m_pAirExhaustSound = controller.SoundCreate( filter, entindex(), "NPC_CombineGunship.ExhaustSound" ); + m_pAirBlastSound = controller.SoundCreate( filter, entindex(), "NPC_CombineGunship.RotorBlastSound" ); + + controller.Play( m_pCannonSound, 0.0, 100 ); + controller.Play( m_pAirExhaustSound, 0.0, 100 ); + controller.Play( m_pAirBlastSound, 0.0, 100 ); + + BaseClass::InitializeRotorSound(); +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::UpdateRotorSoundPitch( int iPitch ) +{ + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + // Apply the pitch to both sounds. + controller.SoundChangePitch( m_pAirExhaustSound, iPitch, 0.1 ); + + // FIXME: Doesn't work in multiplayer + CBaseEntity *pPlayer = UTIL_PlayerByIndex(1); + if (pPlayer) + { + Vector pos; + Vector up; + GetAttachment( "rotor", pos, NULL, NULL, &up ); + + float flDistance = (pPlayer->WorldSpaceCenter() - pos).Length2DSqr(); + + // Fade in exhaust when we're far from the player + float flVolume = clamp( RemapVal( flDistance, (900*900), (1800*1800), 1, 0 ), 0, 1 ); + controller.SoundChangeVolume( m_pAirExhaustSound, flVolume * GetRotorVolume(), 0.1 ); + + // Fade in the blast when it's close to the player (in 2D) + flVolume = clamp( RemapVal( flDistance, (600*600), (700*700), 1, 0 ), 0, 1 ); + controller.SoundChangeVolume( m_pAirBlastSound, flVolume * GetRotorVolume(), 0.1 ); + } + + BaseClass::UpdateRotorSoundPitch( iPitch ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::ApplySidewaysDrag( const Vector &vecRight ) +{ + Vector vecVelocity = GetAbsVelocity(); + if( m_lifeState == LIFE_ALIVE ) + { + vecVelocity.x *= (1.0 - fabs( vecRight.x ) * 0.04); + vecVelocity.y *= (1.0 - fabs( vecRight.y ) * 0.04); + vecVelocity.z *= (1.0 - fabs( vecRight.z ) * 0.04); + } + else + { + vecVelocity.x *= (1.0 - fabs( vecRight.x ) * 0.03); + vecVelocity.y *= (1.0 - fabs( vecRight.y ) * 0.03); + vecVelocity.z *= (1.0 - fabs( vecRight.z ) * 0.09); + } + SetAbsVelocity( vecVelocity ); +} + +//------------------------------------------------------------------------------ +// Purpose: Explode the gunship. +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::SelfDestruct( void ) +{ + SetThink( NULL ); + m_lifeState = LIFE_DEAD; + + StopLoopingSounds(); + StopCannonBurst(); + + Vector vecVelocity = GetAbsVelocity(); + vecVelocity.z = 0.0; // stop falling. + SetAbsVelocity( vecVelocity ); + + CBaseEntity *pBreakEnt = this; + + // If we've ragdolled, play the explosions on the ragdoll instead + Vector vecOrigin; + if ( m_hRagdoll ) + { + m_hRagdoll->EmitSound( "NPC_CombineGunship.Explode" ); + vecOrigin = m_hRagdoll->GetAbsOrigin(); + pBreakEnt = m_hRagdoll; + } + else + { + EmitSound( "NPC_CombineGunship.Explode" ); + vecOrigin = GetAbsOrigin(); + } + + // Create some explosions on the gunship body + Vector vecDelta; + for( int i = 0 ; i < 6 ; i++ ) + { + vecDelta = RandomVector( -200,200 ); + ExplosionCreate( vecOrigin + vecDelta, QAngle( -90, 0, 0 ), this, 10, 10, false ); + } + + AR2Explosion *pExplosion = AR2Explosion::CreateAR2Explosion( vecOrigin ); + if ( pExplosion ) + { + pExplosion->SetLifetime( 10 ); + } + + // If we don't have a crash target, explode into chunks + if ( !m_hCrashTarget ) + { + Vector angVelocity; + QAngleToAngularImpulse( pBreakEnt->GetLocalAngularVelocity(), angVelocity ); + PropBreakableCreateAll( pBreakEnt->GetModelIndex(), pBreakEnt->VPhysicsGetObject(), pBreakEnt->GetAbsOrigin(), pBreakEnt->GetAbsAngles(), pBreakEnt->GetAbsVelocity(), angVelocity, 1.0, 800, COLLISION_GROUP_NPC, pBreakEnt ); + + // Throw out some small chunks too + CPVSFilter filter( GetAbsOrigin() ); + for ( int i = 0; i < 20; i++ ) + { + Vector gibVelocity = RandomVector(-100,100) * 10; + int iModelIndex = modelinfo->GetModelIndex( g_PropDataSystem.GetRandomChunkModel( "MetalChunks" ) ); + te->BreakModel( filter, 0.0, GetAbsOrigin(), vec3_angle, Vector(40,40,40), gibVelocity, iModelIndex, 400, 1, 2.5, BREAK_METAL ); + } + + if ( m_hRagdoll ) + { + UTIL_Remove( m_hRagdoll ); + } + } + else + { + if ( m_pSmokeTrail ) + { + // If we have a ragdoll, let it smoke for a few more seconds + if ( m_hRagdoll ) + { + m_pSmokeTrail->SetLifetime(3.0f); + } + else + { + m_pSmokeTrail->SetLifetime(0.1f); + } + m_pSmokeTrail = NULL; + } + } + + UTIL_Remove( this ); + + // Record this so a nearby citizen can respond. + if ( GetCitizenResponse() ) + { + GetCitizenResponse()->AddResponseTrigger( CR_PLAYER_KILLED_GUNSHIP ); + } + +#ifdef HL2_EPISODIC + NPCEventResponse()->TriggerEvent( "TLK_CITIZEN_RESPONSE_KILLED_GUNSHIP", false, false ); +#endif +} + + +//------------------------------------------------------------------------------ +// Purpose : Explode the gunship. +// Input : +// Output : +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::InputSelfDestruct( inputdata_t &inputdata ) +{ + BeginCrash(); +} + +//------------------------------------------------------------------------------ +// Purpose : Shrink the gunship's bbox so that it fits in docking bays +// Input : +// Output : +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::InputSetDockingBBox( inputdata_t &inputdata ) +{ + Vector vecSize( 32, 32, 32 ); + + UTIL_SetSize( this, vecSize * -1, vecSize ); +} + +//------------------------------------------------------------------------------ +// Purpose : Set the gunship BBox to normal size +// Input : +// Output : +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::InputSetNormalBBox( inputdata_t &inputdata ) +{ + Vector vecBBMin, vecBBMax; + + ExtractBbox( SelectHeaviestSequence( ACT_GUNSHIP_PATROL ), vecBBMin, vecBBMax ); + + // Trim the bounding box a bit. It's huge. +#define GUNSHIP_TRIM_BOX 38 + vecBBMin.x += GUNSHIP_TRIM_BOX; + vecBBMax.x -= GUNSHIP_TRIM_BOX; + vecBBMin.y += GUNSHIP_TRIM_BOX; + vecBBMax.y -= GUNSHIP_TRIM_BOX; + + UTIL_SetSize( this, vecBBMin, vecBBMax ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::InputEnableGroundAttack( inputdata_t &inputdata ) +{ + m_bCanGroundAttack = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::InputDisableGroundAttack( inputdata_t &inputdata ) +{ + m_bCanGroundAttack = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::InputDoGroundAttack( inputdata_t &inputdata ) +{ + // Was a target node specified? + CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, inputdata.value.StringID(), NULL, inputdata.pActivator, inputdata.pCaller ); + if ( pEntity ) + { + // Mapmaker wants us to ground attack a specific target + m_hGroundAttackTarget = pEntity; + } + else + { + StartGroundAttack(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &vGunPosition - +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::UpdateEnemyTarget( void ) +{ + Vector vGunPosition; + + GetAttachment( "muzzle", vGunPosition ); + + // Follow mode + Vector enemyPos; + bool bTargettingPlayer; + + if ( GetEnemy() != NULL ) + { + CBaseCombatCharacter *pCCEnemy = GetEnemy()->MyCombatCharacterPointer(); + if ( pCCEnemy != NULL && pCCEnemy->IsInAVehicle() ) + { + // Update against a driving target + enemyPos = GetEnemy()->WorldSpaceCenter(); + } + else + { + enemyPos = GetEnemy()->EyePosition(); + } + bTargettingPlayer = GetEnemy()->IsPlayer(); + } + else + { + enemyPos = m_vecAttackPosition; + bTargettingPlayer = false; + } + + // Direction towards the enemy + Vector targetDir = enemyPos - m_vecAttackPosition; + VectorNormalize( targetDir ); + + // Direction from the gunship to the enemy + Vector enemyDir = enemyPos - vGunPosition; + VectorNormalize( enemyDir ); + + float lastSpeed = VectorNormalize( m_vecAttackVelocity ); + QAngle chaseAngles, lastChaseAngles; + + VectorAngles( targetDir, chaseAngles ); + VectorAngles( m_vecAttackVelocity, lastChaseAngles ); + + // Debug info + if ( g_debug_gunship.GetInt() == GUNSHIP_DEBUG_STITCHING ) + { + // Final position + NDebugOverlay::Cross3D( m_vecAttackPosition, -Vector(2,2,2), Vector(2,2,2), 0, 0, 255, true, 4.0f ); + } + + float yawDiff = UTIL_AngleDiff( lastChaseAngles[YAW], chaseAngles[YAW] ); + + int maxYaw; + if ( bTargettingPlayer ) + { + maxYaw = 6; + } + else + { + maxYaw = 30; + } + + yawDiff = clamp( yawDiff, -maxYaw, maxYaw ); + + chaseAngles[PITCH] = 0.0f; + chaseAngles[ROLL] = 0.0f; + + bool bMaxHits = ( m_iBurstHits >= GUNSHIP_MAX_HITS_PER_BURST || (GetEnemy() && !GetEnemy()->IsAlive()) ); + + if ( bMaxHits ) + { + // We've hit our target. Stop chasing, and return to max speed. + chaseAngles[YAW] = lastChaseAngles[YAW]; + lastSpeed = BASE_STITCH_VELOCITY; + } + else + { + // Move towards the target yaw + chaseAngles[YAW] = UTIL_AngleMod( lastChaseAngles[YAW] - yawDiff ); + } + + // If we've hit the target already, or we're not close enough to it, then just stitch along + if ( bMaxHits || ( m_vecAttackPosition - enemyPos ).LengthSqr() > (64 * 64) ) + { + AngleVectors( chaseAngles, &targetDir ); + + // Update our new velocity + m_vecAttackVelocity = targetDir * lastSpeed; + + if ( g_debug_gunship.GetInt() == GUNSHIP_DEBUG_STITCHING ) + { + NDebugOverlay::Line( m_vecAttackPosition, m_vecAttackPosition + (m_vecAttackVelocity * 0.1), 255, 0, 0, true, 4.0f ); + } + + // Move along that velocity for this step in time + m_vecAttackPosition += ( m_vecAttackVelocity * 0.1f ); + m_vecAttackPosition.z = enemyPos.z; + } + else + { + // Otherwise always continue to hit an NPC when close enough + m_vecAttackPosition = enemyPos; + } +} + +//------------------------------------------------------------------------------ +// Purpose: Utility function to aim the helicopter gun at the direction +//------------------------------------------------------------------------------ +bool CNPC_CombineGunship::PoseGunTowardTargetDirection( const Vector &vTargetDir ) +{ + Vector vecOut; + VectorIRotate( vTargetDir, EntityToWorldTransform(), vecOut ); + + QAngle angles; + VectorAngles(vecOut, angles); + angles.y = AngleNormalize( angles.y ); + angles.x = AngleNormalize( angles.x ); + + if (angles.x > m_angGun.x) + { + m_angGun.x = MIN( angles.x, m_angGun.x + 12 ); + } + if (angles.x < m_angGun.x) + { + m_angGun.x = MAX( angles.x, m_angGun.x - 12 ); + } + if (angles.y > m_angGun.y) + { + m_angGun.y = MIN( angles.y, m_angGun.y + 12 ); + } + if (angles.y < m_angGun.y) + { + m_angGun.y = MAX( angles.y, m_angGun.y - 12 ); + } + + SetPoseParameter( m_poseWeapon_Pitch, -m_angGun.x ); + SetPoseParameter( m_poseWeapon_Yaw, m_angGun.y ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Vector +//----------------------------------------------------------------------------- +Vector CNPC_CombineGunship::GetMissileTarget( void ) +{ + return GetEnemy()->GetAbsOrigin(); +} + +//------------------------------------------------------------------------------ +// Purpose : Get the target position for the enemy- the position we fire upon. +// this is often modified by m_flAttackOffset to provide the 'stitching' +// behavior that's so popular with the kids these days (sjb) +// +// Input : vGunPosition - location of gunship's muzzle +// : pTarget = vector to paste enemy target into. +// Output : +//------------------------------------------------------------------------------ +Vector CNPC_CombineGunship::GetEnemyTarget( void ) +{ + // Make sure we have an enemy + if ( GetEnemy() == NULL ) + return m_vecAttackPosition; + + // If we're locked onto a missile, use special code to try and destroy it + if ( IsTargettingMissile() ) + return GetMissileTarget(); + + return m_vecAttackPosition; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &tr - +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::DoImpactEffect( trace_t &tr, int nDamageType ) +{ + UTIL_ImpactTrace( &tr, nDamageType, "ImpactGunship" ); + + // These glow effects don't sort properly, so they're cut for E3 2003 (sjb) +#if 0 + CEffectData data; + + data.m_vOrigin = tr.endpos; + data.m_vNormal = vec3_origin; + data.m_vAngles = vec3_angle; + + DispatchEffect( "GunshipImpact", data ); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Make the gunship's signature blue tracer! +// Input : &vecTracerSrc - +// &tr - +// iTracerType - +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::MakeTracer( const Vector &vecTracerSrc, const trace_t &tr, int iTracerType ) +{ + switch ( iTracerType ) + { + case TRACER_LINE: + { + float flTracerDist; + Vector vecDir; + Vector vecEndPos; + + vecDir = tr.endpos - vecTracerSrc; + + flTracerDist = VectorNormalize( vecDir ); + + UTIL_Tracer( vecTracerSrc, tr.endpos, 0, TRACER_DONT_USE_ATTACHMENT, 8000, true, "GunshipTracer" ); + } + break; + + default: + BaseClass::MakeTracer( vecTracerSrc, tr, iTracerType ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &info - +// &vecDir - +// *ptr - +// Output : int +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + // Reflect bullets + if ( info.GetDamageType() & DMG_BULLET ) + { + if ( random->RandomInt( 0, 2 ) == 0 ) + { + Vector vecRicochetDir = vecDir * -1; + + vecRicochetDir.x += random->RandomFloat( -0.5, 0.5 ); + vecRicochetDir.y += random->RandomFloat( -0.5, 0.5 ); + vecRicochetDir.z += random->RandomFloat( -0.5, 0.5 ); + + VectorNormalize( vecRicochetDir ); + + Vector end = ptr->endpos + vecRicochetDir * 1024; + UTIL_Tracer( ptr->endpos, end, entindex(), TRACER_DONT_USE_ATTACHMENT, 3500 ); + } + + // If this is from a player, record it so a nearby citizen can respond. + if ( info.GetAttacker()->IsPlayer() ) + { + if ( GetCitizenResponse() ) + { + GetCitizenResponse()->AddResponseTrigger( CR_PLAYER_SHOT_GUNSHIP ); + } + +#ifdef HL2_EPISODIC + NPCEventResponse()->TriggerEvent( "TLK_CITIZEN_RESPONSE_SHOT_GUNSHIP", false, false ); +#endif + } + + return; + } + + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + +//----------------------------------------------------------------------------- +// Purpose: This is necessary to ensure that the game doesn't break if a mapmaker has outputs that +// must be fired on gunships, and the player switches skill levels +// midway through a gunship battle. +// Input : iDamageNumber - +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::FireDamageOutputsUpto( int iDamageNumber ) +{ + for ( int i = 0; i <= iDamageNumber; i++ ) + { + if ( !m_bDamageOutputsFired[i] ) + { + m_bDamageOutputsFired[i] = true; + + switch ( i ) + { + case 0: + //Msg("Fired first\n"); + m_OnFirstDamage.FireOutput( this, this ); + break; + + case 1: + //Msg("Fired second\n"); + m_OnSecondDamage.FireOutput( this, this ); + break; + + case 2: + //Msg("Fired third\n"); + m_OnThirdDamage.FireOutput( this, this ); + break; + + case 3: + //Msg("Fired fourth\n"); + m_OnFourthDamage.FireOutput( this, this ); + break; + } + } + } +} + +//------------------------------------------------------------------------------ +// Damage filtering +//------------------------------------------------------------------------------ +int CNPC_CombineGunship::OnTakeDamage_Alive( const CTakeDamageInfo &inputInfo ) +{ + // Allow npc_kill to kill me + if ( inputInfo.GetDamageType() != DMG_GENERIC ) + { + // Ignore mundane bullet damage. + if ( ( inputInfo.GetDamageType() & DMG_BLAST ) == false ) + return 0; + + // Ignore blasts less than this amount + if ( inputInfo.GetDamage() < GUNSHIP_MIN_DAMAGE_THRESHOLD ) + return 0; + } + + // Only take blast damage + CTakeDamageInfo info = inputInfo; + + // Make a pain sound + if ( !HasSpawnFlags( SF_GUNSHIP_USE_CHOPPER_MODEL ) ) + { + EmitSound( "NPC_CombineGunship.Pain" ); + } + + Vector damageDir = info.GetDamageForce(); + VectorNormalize( damageDir ); + + // Don't get knocked around if I'm ground attacking + if ( !m_bIsGroundAttacking ) + { + ApplyAbsVelocityImpulse( damageDir * 200.0f ); + } + + if ( m_bInvulnerable == false ) + { + // Take a percentage of our health away + // Adjust health for damage + int iHealthIncrements = sk_gunship_health_increments.GetInt(); + if ( g_pGameRules->IsSkillLevel( SKILL_EASY ) ) + { + iHealthIncrements = ceil( iHealthIncrements * 0.5 ); + } + else if ( g_pGameRules->IsSkillLevel( SKILL_HARD ) ) + { + iHealthIncrements = floor( iHealthIncrements * 1.5 ); + } + info.SetDamage( ( GetMaxHealth() / (float)iHealthIncrements ) + 1 ); + + // Find out which "stage" we're at in our health + int healthIncrement = iHealthIncrements - ( GetHealth() / (float)(( GetMaxHealth() / (float)iHealthIncrements )) ); + switch ( healthIncrement ) + { + case 1: + // If we're on Easy, we're half dead now, so fire the rest of our outputs too + // This is done in case the mapmaker's connected those inputs to something important + // that has to happen before the gunship dies. + if ( g_pGameRules->IsSkillLevel( SKILL_EASY ) ) + { + FireDamageOutputsUpto( 3 ); + } + else + { + FireDamageOutputsUpto( 1 ); + } + break; + + default: + FireDamageOutputsUpto( healthIncrement ); + break; + } + + // Start smoking when we're almost dead + CreateSmokeTrail(); + + if ( m_pSmokeTrail ) + { + if ( healthIncrement < 2 ) + { + m_pSmokeTrail->SetLifetime( 8.0 ); + } + + m_pSmokeTrail->FollowEntity( this, "exhaustl" ); + } + + // Move with the target + Vector gibVelocity = GetAbsVelocity() + (-damageDir * 200.0f); + + // Dump out metal gibs + CPVSFilter filter( GetAbsOrigin() ); + for ( int i = 0; i < 10; i++ ) + { + int iModelIndex = modelinfo->GetModelIndex( g_PropDataSystem.GetRandomChunkModel( "MetalChunks" ) ); + te->BreakModel( filter, 0.0, GetAbsOrigin(), vec3_angle, Vector(40,40,40), gibVelocity, iModelIndex, 400, 1, 2.5, BREAK_METAL ); + } + } + + return BaseClass::OnTakeDamage_Alive( info ); +} + + +//------------------------------------------------------------------------------ +// Purpose : The proper way to begin the gunship cannon firing at the enemy. +// Input : iBurstSize - the size of the burst, in rounds. +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::StartCannonBurst( int iBurstSize ) +{ + m_iBurstSize = iBurstSize; + m_iBurstHits = 0; + + m_flTimeNextAttack = gpGlobals->curtime; + + // Start up the cannon sound. + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + controller.SoundChangeVolume( m_pCannonSound, 1.0, 0 ); + + m_bIsFiring = true; + + // Setup the initial position of the burst + if ( GetEnemy() ) + { + // Follow mode + Vector enemyPos; + UTIL_PredictedPosition( GetEnemy(), 2.0f, &enemyPos ); + + QAngle offsetAngles; + Vector offsetDir = ( WorldSpaceCenter() - enemyPos ); + VectorNormalize( offsetDir ); + VectorAngles( offsetDir, offsetAngles ); + + int angleOffset = random->RandomInt( 15, 30 ); + if ( random->RandomInt( 0, 1 ) ) + { + angleOffset *= -1; + } + offsetAngles[YAW] += angleOffset; + offsetAngles[PITCH] = 0; + offsetAngles[ROLL] = 0; + + AngleVectors( offsetAngles, &offsetDir ); + + float stitchOffset; + float enemyDist = GroundDistToPosition( GetEnemy()->GetAbsOrigin() ); + if ( enemyDist < ( sk_gunship_burst_dist.GetFloat() + GUNSHIP_STITCH_MIN ) ) + { + stitchOffset = GUNSHIP_STITCH_MIN; + } + else + { + stitchOffset = sk_gunship_burst_dist.GetFloat(); + } + + // Move out to the start of our stitch run + m_vecAttackPosition = enemyPos + ( offsetDir * stitchOffset ); + m_vecAttackPosition.z = enemyPos.z; + + // Point at our target + m_vecAttackVelocity = -offsetDir * BASE_STITCH_VELOCITY; + + CSoundEnt::InsertSound( SOUND_DANGER | SOUND_CONTEXT_REACT_TO_SOURCE, enemyPos, 512, 0.2f, this ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose : The proper way to cease the gunship cannon firing. +//------------------------------------------------------------------------------ +void CNPC_CombineGunship::StopCannonBurst( void ) +{ + m_iBurstHits = 0; + m_bIsFiring = false; + m_bPreFire = false; + + // Reduce the burst time when we get lower in health + float flPerc = (float)GetHealth() / (float)GetMaxHealth(); + float flDelay = clamp( flPerc * m_flBurstDelay, 0.5, m_flBurstDelay ); + + // If we didn't finish the burst, don't wait so long + flPerc = 1.0 - (m_iBurstSize / sk_gunship_burst_size.GetFloat()); + flDelay *= flPerc; + + m_flTimeNextAttack = gpGlobals->curtime + flDelay; + m_iBurstSize = 0; + + // Stop the cannon sound. + if ( m_pCannonSound != NULL ) + { + CSoundEnvelopeController::GetController().SoundChangeVolume( m_pCannonSound, 0.0, 0.05 ); + } + + EmitSound( "NPC_CombineGunship.CannonStopSound" ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::StopLoopingSounds( void ) +{ + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + if ( m_pCannonSound ) + { + controller.SoundDestroy( m_pCannonSound ); + m_pCannonSound = NULL; + } + + if ( m_pRotorSound ) + { + controller.SoundDestroy( m_pRotorSound ); + m_pRotorSound = NULL; + } + + if ( m_pAirExhaustSound ) + { + controller.SoundDestroy( m_pAirExhaustSound ); + m_pAirExhaustSound = NULL; + } + + if ( m_pAirBlastSound ) + { + controller.SoundDestroy( m_pAirBlastSound ); + m_pAirBlastSound = NULL; + } + + BaseClass::StopLoopingSounds(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEnemy - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_CombineGunship::IsValidEnemy( CBaseEntity *pEnemy ) +{ + // Always track missiles + if ( pEnemy->IsAlive() && !pEnemy->MyNPCPointer() && FClassnameIs( pEnemy, "rpg_missile" ) ) + return true; + + // If we're shooting off a burst, don't pick up a new enemy + if ( ( m_bIsFiring ) && ( ( GetEnemy() == NULL ) || ( GetEnemy() != pEnemy ) ) ) + return false; + + return BaseClass::IsValidEnemy( pEnemy ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::GatherEnemyConditions( CBaseEntity *pEnemy ) +{ + BaseClass::GatherEnemyConditions(pEnemy); + + // If we can't see the enemy for a few seconds, consider him unreachable + if ( !HasCondition(COND_SEE_ENEMY) ) + { + if ( gpGlobals->curtime - GetEnemyLastTimeSeen() >= 3.0f ) + { + MarkEnemyAsEluded(); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Tells us whether or not we're targetting an incoming missile +//----------------------------------------------------------------------------- +bool CNPC_CombineGunship::IsTargettingMissile( void ) +{ + if ( GetEnemy() == NULL ) + return false; + + if ( FClassnameIs( GetEnemy(), "rpg_missile" ) == false ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::InputBecomeInvulnerable( inputdata_t &input ) +{ + m_bInvulnerable = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_CombineGunship::InputBecomeVulnerable( inputdata_t &input ) +{ + m_bInvulnerable = false; +} + +AI_BEGIN_CUSTOM_NPC( npc_combinegunship, CNPC_CombineGunship ) + +// DECLARE_TASK( ) + + DECLARE_ACTIVITY( ACT_GUNSHIP_PATROL ); + DECLARE_ACTIVITY( ACT_GUNSHIP_HOVER ); + DECLARE_ACTIVITY( ACT_GUNSHIP_CRASH ); + + //DECLARE_CONDITION( COND_ ) + + //========================================================= +// DEFINE_SCHEDULE +// ( +// SCHED_DUMMY, +// +// " Tasks" +// " TASK_FACE_ENEMY 0" +// " " +// " Interrupts" +// ) + + +AI_END_CUSTOM_NPC() + |