sqwarmed/sdk_src/game/server/hl2/npc_combinegunship.cpp

3246 lines
97 KiB
C++

//========= Copyright (c) 1996-2005, 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 int 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 );
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_fMaxAngAcceleration;
Vector m_vMaxAngVelocity;
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_KEYFIELD( m_fMaxAngAcceleration, FIELD_FLOAT, "MaxAngAccel" ),
DEFINE_KEYFIELD( m_vMaxAngVelocity, FIELD_VECTOR, "MaxAngVelocity" ),
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;
m_fMaxAngAcceleration = 1000.0f;
m_vMaxAngVelocity = Vector( 300.0f, 120.0f, 300.0f );
}
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_flFrozenMax = 0.0f;
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" );
PrecacheEffect( "ImpactGunship" );
PrecacheEffect( "AR2Explosion" );
PrecacheEffect( "GunshipMuzzleFlash" );
PrecacheEffect( "GunshipTracer" );
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_flPlayerDamage = 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_flPlayerDamage = 1.0f;
}
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, -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, -m_fMaxAngAcceleration, m_fMaxAngAcceleration );
angAccelAccel.y = clamp( angAccelAccel.y, -m_fMaxAngAcceleration, m_fMaxAngAcceleration );
angAccelAccel.z = clamp( angAccelAccel.z, -m_fMaxAngAcceleration, m_fMaxAngAcceleration );
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.x = clamp( angVel.x, -m_vMaxAngVelocity.x, m_vMaxAngVelocity.x );
angVel.y = clamp( angVel.y, -m_vMaxAngVelocity.y, m_vMaxAngVelocity.y );
angVel.z = clamp( angVel.z, -m_vMaxAngVelocity.z, m_vMaxAngVelocity.z );
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 );
#ifndef INFESTED_DLL
// Record this so a nearby citizen can respond.
if ( GetCitizenResponse() )
{
GetCitizenResponse()->AddResponseTrigger( CR_PLAYER_KILLED_GUNSHIP );
}
#endif
#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 )
{
// 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() )
{
#ifndef INFESTED_DLL
if ( GetCitizenResponse() )
{
GetCitizenResponse()->AddResponseTrigger( CR_PLAYER_SHOT_GUNSHIP );
}
#endif
#ifdef HL2_EPISODIC
NPCEventResponse()->TriggerEvent( "TLK_CITIZEN_RESPONSE_SHOT_GUNSHIP", false, false );
#endif
}
return;
}
BaseClass::TraceAttack( info, vecDir, ptr );
}
//-----------------------------------------------------------------------------
// 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()