You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1105 lines
33 KiB
1105 lines
33 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
//=============================================================================// |
|
#include "cbase.h" |
|
#include "vehicle_apc.h" |
|
#include "ammodef.h" |
|
#include "IEffects.h" |
|
#include "engine/IEngineSound.h" |
|
#include "weapon_rpg.h" |
|
#include "in_buttons.h" |
|
#include "globalstate.h" |
|
#include "soundent.h" |
|
#include "ai_basenpc.h" |
|
#include "ndebugoverlay.h" |
|
#include "gib.h" |
|
#include "EntityFlame.h" |
|
#include "smoke_trail.h" |
|
#include "explode.h" |
|
#include "effect_dispatch_data.h" |
|
#include "te_effect_dispatch.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
|
|
#define ROCKET_ATTACK_RANGE_MAX 5500.0f |
|
#define ROCKET_ATTACK_RANGE_MIN 1250.0f |
|
|
|
#define MACHINE_GUN_ATTACK_RANGE_MAX 1250.0f |
|
#define MACHINE_GUN_ATTACK_RANGE_MIN 0.0f |
|
|
|
#define MACHINE_GUN_MAX_UP_PITCH 30 |
|
#define MACHINE_GUN_MAX_DOWN_PITCH 10 |
|
#define MACHINE_GUN_MAX_LEFT_YAW 30 |
|
#define MACHINE_GUN_MAX_RIGHT_YAW 30 |
|
|
|
#define MACHINE_GUN_BURST_SIZE 10 |
|
#define MACHINE_GUN_BURST_TIME 0.075f |
|
#define MACHINE_GUN_BURST_PAUSE_TIME 2.0f |
|
|
|
#define ROCKET_SALVO_SIZE 5 |
|
#define ROCKET_DELAY_TIME 1.5 |
|
#define ROCKET_MIN_BURST_PAUSE_TIME 3 |
|
#define ROCKET_MAX_BURST_PAUSE_TIME 4 |
|
#define ROCKET_SPEED 800 |
|
#define DEATH_VOLLEY_ROCKET_COUNT 4 |
|
#define DEATH_VOLLEY_MIN_FIRE_TIME 0.333 |
|
#define DEATH_VOLLEY_MAX_FIRE_TIME 0.166 |
|
|
|
extern short g_sModelIndexFireball; // Echh... |
|
|
|
|
|
ConVar sk_apc_health( "sk_apc_health", "750" ); |
|
|
|
|
|
#define APC_MAX_CHUNKS 3 |
|
static const char *s_pChunkModelName[APC_MAX_CHUNKS] = |
|
{ |
|
"models/gibs/helicopter_brokenpiece_01.mdl", |
|
"models/gibs/helicopter_brokenpiece_02.mdl", |
|
"models/gibs/helicopter_brokenpiece_03.mdl", |
|
}; |
|
|
|
#define APC_MAX_GIBS 6 |
|
static const char *s_pGibModelName[APC_MAX_GIBS] = |
|
{ |
|
"models/combine_apc_destroyed_gib01.mdl", |
|
"models/combine_apc_destroyed_gib02.mdl", |
|
"models/combine_apc_destroyed_gib03.mdl", |
|
"models/combine_apc_destroyed_gib04.mdl", |
|
"models/combine_apc_destroyed_gib05.mdl", |
|
"models/combine_apc_destroyed_gib06.mdl", |
|
}; |
|
|
|
|
|
LINK_ENTITY_TO_CLASS( prop_vehicle_apc, CPropAPC ); |
|
|
|
|
|
BEGIN_DATADESC( CPropAPC ) |
|
|
|
DEFINE_FIELD( m_flDangerSoundTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_flHandbrakeTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_bInitialHandbrake, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_nSmokeTrailCount, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_flMachineGunTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_iMachineGunBurstLeft, FIELD_INTEGER ), |
|
// DEFINE_FIELD( m_nMachineGunMuzzleAttachment, FIELD_INTEGER ), |
|
// DEFINE_FIELD( m_nMachineGunBaseAttachment, FIELD_INTEGER ), |
|
// DEFINE_FIELD( m_vecBarrelPos, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_bInFiringCone, FIELD_BOOLEAN ), |
|
// DEFINE_FIELD( m_hLaserDot, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_hRocketTarget, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_iRocketSalvoLeft, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_flRocketTime, FIELD_TIME ), |
|
// DEFINE_FIELD( m_nRocketAttachment, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_nRocketSide, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_hSpecificRocketTarget, FIELD_EHANDLE ), |
|
DEFINE_KEYFIELD( m_strMissileHint, FIELD_STRING, "missilehint" ), |
|
|
|
DEFINE_INPUTFUNC( FIELD_VOID, "Destroy", InputDestroy ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "FireMissileAt", InputFireMissileAt ), |
|
|
|
DEFINE_OUTPUT( m_OnDeath, "OnDeath" ), |
|
DEFINE_OUTPUT( m_OnFiredMissile, "OnFiredMissile" ), |
|
DEFINE_OUTPUT( m_OnDamaged, "OnDamaged" ), |
|
DEFINE_OUTPUT( m_OnDamagedByPlayer, "OnDamagedByPlayer" ), |
|
|
|
END_DATADESC() |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::Precache( void ) |
|
{ |
|
BaseClass::Precache(); |
|
|
|
int i; |
|
for ( i = 0; i < APC_MAX_CHUNKS; ++i ) |
|
{ |
|
PrecacheModel( s_pChunkModelName[i] ); |
|
} |
|
|
|
for ( i = 0; i < APC_MAX_GIBS; ++i ) |
|
{ |
|
PrecacheModel( s_pGibModelName[i] ); |
|
} |
|
|
|
PrecacheScriptSound( "Weapon_AR2.Single" ); |
|
PrecacheScriptSound( "PropAPC.FireRocket" ); |
|
PrecacheScriptSound( "combine.door_lock" ); |
|
} |
|
|
|
|
|
//------------------------------------------------ |
|
// Spawn |
|
//------------------------------------------------ |
|
void CPropAPC::Spawn( void ) |
|
{ |
|
BaseClass::Spawn(); |
|
SetBlocksLOS( true ); |
|
m_iHealth = m_iMaxHealth = sk_apc_health.GetFloat(); |
|
SetCycle( 0 ); |
|
m_iMachineGunBurstLeft = MACHINE_GUN_BURST_SIZE; |
|
m_iRocketSalvoLeft = ROCKET_SALVO_SIZE; |
|
m_nRocketSide = 0; |
|
m_lifeState = LIFE_ALIVE; |
|
m_bInFiringCone = false; |
|
|
|
m_flHandbrakeTime = gpGlobals->curtime + 0.1; |
|
m_bInitialHandbrake = false; |
|
|
|
// Reset the gun to a default pose. |
|
SetPoseParameter( "vehicle_weapon_pitch", 0 ); |
|
SetPoseParameter( "vehicle_weapon_yaw", 90 ); |
|
|
|
CreateAPCLaserDot(); |
|
|
|
if( g_pGameRules->GetAutoAimMode() == AUTOAIM_ON_CONSOLE ) |
|
{ |
|
AddFlag( FL_AIMTARGET ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Create a laser |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::CreateAPCLaserDot( void ) |
|
{ |
|
// Create a laser if we don't have one |
|
if ( m_hLaserDot == NULL ) |
|
{ |
|
m_hLaserDot = CreateLaserDot( GetAbsOrigin(), this, false ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
bool CPropAPC::ShouldAttractAutoAim( CBaseEntity *pAimingEnt ) |
|
{ |
|
if( g_pGameRules->GetAutoAimMode() == AUTOAIM_ON_CONSOLE && pAimingEnt->IsPlayer() && GetDriver() ) |
|
{ |
|
return true; |
|
} |
|
|
|
return BaseClass::ShouldAttractAutoAim( pAimingEnt ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::Activate() |
|
{ |
|
BaseClass::Activate(); |
|
|
|
m_nRocketAttachment = LookupAttachment( "cannon_muzzle" ); |
|
m_nMachineGunMuzzleAttachment = LookupAttachment( "muzzle" ); |
|
m_nMachineGunBaseAttachment = LookupAttachment( "gun_base" ); |
|
|
|
// NOTE: gun_ref must have the same position as gun_base, but rotates with the gun |
|
int nMachineGunRefAttachment = LookupAttachment( "gun_def" ); |
|
|
|
Vector vecWorldBarrelPos; |
|
matrix3x4_t matRefToWorld; |
|
GetAttachment( m_nMachineGunMuzzleAttachment, vecWorldBarrelPos ); |
|
GetAttachment( nMachineGunRefAttachment, matRefToWorld ); |
|
VectorITransform( vecWorldBarrelPos, matRefToWorld, m_vecBarrelPos ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::UpdateOnRemove( void ) |
|
{ |
|
if ( m_hLaserDot ) |
|
{ |
|
UTIL_Remove( m_hLaserDot ); |
|
m_hLaserDot = NULL; |
|
} |
|
BaseClass::UpdateOnRemove(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::CreateServerVehicle( void ) |
|
{ |
|
// Create our armed server vehicle |
|
m_pServerVehicle = new CAPCFourWheelServerVehicle(); |
|
m_pServerVehicle->SetVehicle( this ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *pMoveData - |
|
//----------------------------------------------------------------------------- |
|
Class_T CPropAPC::ClassifyPassenger( CBaseCombatCharacter *pPassenger, Class_T defaultClassification ) |
|
{ |
|
return CLASS_COMBINE; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Damage events as modified for the passenger of the APC, not the APC itself |
|
//----------------------------------------------------------------------------- |
|
float CPropAPC::PassengerDamageModifier( const CTakeDamageInfo &info ) |
|
{ |
|
CTakeDamageInfo DmgInfo = info; |
|
|
|
// bullets, slashing and headbutts don't hurt us in the apc, neither do rockets |
|
if( (DmgInfo.GetDamageType() & DMG_BULLET) || (DmgInfo.GetDamageType() & DMG_SLASH) || |
|
(DmgInfo.GetDamageType() & DMG_CLUB) || (DmgInfo.GetDamageType() & DMG_BLAST) ) |
|
return (0); |
|
|
|
// Accept everything else by default |
|
return 1.0; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// position of eyes |
|
//----------------------------------------------------------------------------- |
|
Vector CPropAPC::EyePosition( ) |
|
{ |
|
Vector vecEyePosition; |
|
CollisionProp()->NormalizedToWorldSpace( Vector( 0.5, 0.5, 1.0 ), &vecEyePosition ); |
|
return vecEyePosition; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
Vector CPropAPC::BodyTarget( const Vector &posSrc, bool bNoisy ) |
|
{ |
|
if( g_pGameRules->GetAutoAimMode() == AUTOAIM_ON_CONSOLE ) |
|
{ |
|
return WorldSpaceCenter(); |
|
} |
|
|
|
return BaseClass::BodyTarget( posSrc, bNoisy ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Add a smoke trail since we've taken more damage |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::AddSmokeTrail( const Vector &vecPos ) |
|
{ |
|
// Start this trail out with a bang! |
|
ExplosionCreate( vecPos, vec3_angle, this, 1000, 500.0f, SF_ENVEXPLOSION_NODAMAGE | |
|
SF_ENVEXPLOSION_NOSPARKS | SF_ENVEXPLOSION_NODLIGHTS | SF_ENVEXPLOSION_NOSMOKE | |
|
SF_ENVEXPLOSION_NOFIREBALLSMOKE, 0 ); |
|
UTIL_ScreenShake( vecPos, 25.0, 150.0, 1.0, 750.0f, SHAKE_START ); |
|
|
|
if ( m_nSmokeTrailCount == MAX_SMOKE_TRAILS ) |
|
return; |
|
|
|
SmokeTrail *pSmokeTrail = SmokeTrail::CreateSmokeTrail(); |
|
if( !pSmokeTrail ) |
|
return; |
|
|
|
// See if there's an attachment for this smoke trail |
|
char buf[32]; |
|
Q_snprintf( buf, 32, "damage%d", m_nSmokeTrailCount ); |
|
int nAttachment = LookupAttachment( buf ); |
|
|
|
++m_nSmokeTrailCount; |
|
|
|
pSmokeTrail->m_SpawnRate = 4; |
|
pSmokeTrail->m_ParticleLifetime = 5.0f; |
|
pSmokeTrail->m_StartColor.Init( 0.7f, 0.7f, 0.7f ); |
|
pSmokeTrail->m_EndColor.Init( 0.6, 0.6, 0.6 ); |
|
pSmokeTrail->m_StartSize = 32; |
|
pSmokeTrail->m_EndSize = 64; |
|
pSmokeTrail->m_SpawnRadius = 4; |
|
pSmokeTrail->m_Opacity = 0.5f; |
|
pSmokeTrail->m_MinSpeed = 16; |
|
pSmokeTrail->m_MaxSpeed = 16; |
|
pSmokeTrail->m_MinDirectedSpeed = 16.0f; |
|
pSmokeTrail->m_MaxDirectedSpeed = 16.0f; |
|
pSmokeTrail->SetLifetime( 5 ); |
|
pSmokeTrail->SetParent( this, nAttachment ); |
|
|
|
Vector vecForward( 0, 0, 1 ); |
|
QAngle angles; |
|
VectorAngles( vecForward, angles ); |
|
|
|
if ( nAttachment == 0 ) |
|
{ |
|
pSmokeTrail->SetAbsOrigin( vecPos ); |
|
pSmokeTrail->SetAbsAngles( angles ); |
|
} |
|
else |
|
{ |
|
pSmokeTrail->SetLocalOrigin( vec3_origin ); |
|
pSmokeTrail->SetLocalAngles( angles ); |
|
} |
|
|
|
pSmokeTrail->SetMoveType( MOVETYPE_NONE ); |
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------ |
|
// Pow! |
|
//------------------------------------------------------------------------------ |
|
void CPropAPC::ExplodeAndThrowChunk( const Vector &vecExplosionPos ) |
|
{ |
|
ExplosionCreate( vecExplosionPos, vec3_angle, this, 1000, 500.0f, |
|
SF_ENVEXPLOSION_NODAMAGE | SF_ENVEXPLOSION_NOSPARKS | SF_ENVEXPLOSION_NODLIGHTS | |
|
SF_ENVEXPLOSION_NOSMOKE | SF_ENVEXPLOSION_NOFIREBALLSMOKE, 0 ); |
|
UTIL_ScreenShake( vecExplosionPos, 25.0, 150.0, 1.0, 750.0f, SHAKE_START ); |
|
|
|
// Drop a flaming, smoking chunk. |
|
CGib *pChunk = CREATE_ENTITY( CGib, "gib" ); |
|
pChunk->Spawn( "models/gibs/hgibs.mdl" ); |
|
pChunk->SetBloodColor( DONT_BLEED ); |
|
|
|
QAngle vecSpawnAngles; |
|
vecSpawnAngles.Random( -90, 90 ); |
|
pChunk->SetAbsOrigin( vecExplosionPos ); |
|
pChunk->SetAbsAngles( vecSpawnAngles ); |
|
|
|
int nGib = random->RandomInt( 0, APC_MAX_CHUNKS - 1 ); |
|
pChunk->Spawn( s_pChunkModelName[nGib] ); |
|
pChunk->SetOwnerEntity( this ); |
|
pChunk->m_lifeTime = random->RandomFloat( 6.0f, 8.0f ); |
|
pChunk->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); |
|
IPhysicsObject *pPhysicsObject = pChunk->VPhysicsInitNormal( SOLID_VPHYSICS, pChunk->GetSolidFlags(), false ); |
|
|
|
// Set the velocity |
|
if ( pPhysicsObject ) |
|
{ |
|
pPhysicsObject->EnableMotion( true ); |
|
Vector vecVelocity; |
|
|
|
QAngle angles; |
|
angles.x = random->RandomFloat( -40, 0 ); |
|
angles.y = random->RandomFloat( 0, 360 ); |
|
angles.z = 0.0f; |
|
AngleVectors( angles, &vecVelocity ); |
|
|
|
vecVelocity *= random->RandomFloat( 300, 900 ); |
|
vecVelocity += GetAbsVelocity(); |
|
|
|
AngularImpulse angImpulse; |
|
angImpulse = RandomAngularImpulse( -180, 180 ); |
|
|
|
pChunk->SetAbsVelocity( vecVelocity ); |
|
pPhysicsObject->SetVelocity(&vecVelocity, &angImpulse ); |
|
} |
|
|
|
CEntityFlame *pFlame = CEntityFlame::Create( pChunk, false ); |
|
if ( pFlame != NULL ) |
|
{ |
|
pFlame->SetLifetime( pChunk->m_lifeTime ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Should we trigger a damage effect? |
|
//----------------------------------------------------------------------------- |
|
inline bool CPropAPC::ShouldTriggerDamageEffect( int nPrevHealth, int nEffectCount ) const |
|
{ |
|
int nPrevRange = (int)( ((float)nPrevHealth / (float)GetMaxHealth()) * nEffectCount ); |
|
int nRange = (int)( ((float)GetHealth() / (float)GetMaxHealth()) * nEffectCount ); |
|
return ( nRange != nPrevRange ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::Event_Killed( const CTakeDamageInfo &info ) |
|
{ |
|
m_OnDeath.FireOutput( info.GetAttacker(), this ); |
|
|
|
Vector vecAbsMins, vecAbsMaxs; |
|
CollisionProp()->WorldSpaceAABB( &vecAbsMins, &vecAbsMaxs ); |
|
|
|
Vector vecNormalizedMins, vecNormalizedMaxs; |
|
CollisionProp()->WorldToNormalizedSpace( vecAbsMins, &vecNormalizedMins ); |
|
CollisionProp()->WorldToNormalizedSpace( vecAbsMaxs, &vecNormalizedMaxs ); |
|
|
|
Vector vecAbsPoint; |
|
CPASFilter filter( GetAbsOrigin() ); |
|
for (int i = 0; i < 5; i++) |
|
{ |
|
CollisionProp()->RandomPointInBounds( vecNormalizedMins, vecNormalizedMaxs, &vecAbsPoint ); |
|
te->Explosion( filter, random->RandomFloat( 0.0, 1.0 ), &vecAbsPoint, |
|
g_sModelIndexFireball, random->RandomInt( 4, 10 ), |
|
random->RandomInt( 8, 15 ), |
|
( i < 2 ) ? TE_EXPLFLAG_NODLIGHTS : TE_EXPLFLAG_NOPARTICLES | TE_EXPLFLAG_NOFIREBALLSMOKE | TE_EXPLFLAG_NODLIGHTS, |
|
100, 0 ); |
|
} |
|
|
|
// TODO: make the gibs spawn in sync with the delayed explosions |
|
int nGibs = random->RandomInt( 1, 4 ); |
|
for ( int i = 0; i < nGibs; i++) |
|
{ |
|
// Throw a flaming, smoking chunk. |
|
CGib *pChunk = CREATE_ENTITY( CGib, "gib" ); |
|
pChunk->Spawn( "models/gibs/hgibs.mdl" ); |
|
pChunk->SetBloodColor( DONT_BLEED ); |
|
|
|
QAngle vecSpawnAngles; |
|
vecSpawnAngles.Random( -90, 90 ); |
|
pChunk->SetAbsOrigin( vecAbsPoint ); |
|
pChunk->SetAbsAngles( vecSpawnAngles ); |
|
|
|
int nGib = random->RandomInt( 0, APC_MAX_CHUNKS - 1 ); |
|
pChunk->Spawn( s_pChunkModelName[nGib] ); |
|
pChunk->SetOwnerEntity( this ); |
|
pChunk->m_lifeTime = random->RandomFloat( 6.0f, 8.0f ); |
|
pChunk->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); |
|
IPhysicsObject *pPhysicsObject = pChunk->VPhysicsInitNormal( SOLID_VPHYSICS, pChunk->GetSolidFlags(), false ); |
|
|
|
// Set the velocity |
|
if ( pPhysicsObject ) |
|
{ |
|
pPhysicsObject->EnableMotion( true ); |
|
Vector vecVelocity; |
|
|
|
QAngle angles; |
|
angles.x = random->RandomFloat( -20, 20 ); |
|
angles.y = random->RandomFloat( 0, 360 ); |
|
angles.z = 0.0f; |
|
AngleVectors( angles, &vecVelocity ); |
|
|
|
vecVelocity *= random->RandomFloat( 300, 900 ); |
|
vecVelocity += GetAbsVelocity(); |
|
|
|
AngularImpulse angImpulse; |
|
angImpulse = RandomAngularImpulse( -180, 180 ); |
|
|
|
pChunk->SetAbsVelocity( vecVelocity ); |
|
pPhysicsObject->SetVelocity(&vecVelocity, &angImpulse ); |
|
} |
|
|
|
CEntityFlame *pFlame = CEntityFlame::Create( pChunk, false ); |
|
if ( pFlame != NULL ) |
|
{ |
|
pFlame->SetLifetime( pChunk->m_lifeTime ); |
|
} |
|
} |
|
|
|
UTIL_ScreenShake( vecAbsPoint, 25.0, 150.0, 1.0, 750.0f, SHAKE_START ); |
|
|
|
if( hl2_episodic.GetBool() ) |
|
{ |
|
// EP1 perf hit |
|
Ignite( 6, false ); |
|
} |
|
else |
|
{ |
|
Ignite( 60, false ); |
|
} |
|
|
|
m_lifeState = LIFE_DYING; |
|
|
|
// Spawn a lesser amount if the player is close |
|
m_iRocketSalvoLeft = DEATH_VOLLEY_ROCKET_COUNT; |
|
m_flRocketTime = gpGlobals->curtime; |
|
} |
|
|
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Blows it up! |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::InputDestroy( inputdata_t &inputdata ) |
|
{ |
|
CTakeDamageInfo info( this, this, m_iHealth, DMG_BLAST ); |
|
info.SetDamagePosition( WorldSpaceCenter() ); |
|
info.SetDamageForce( Vector( 0, 0, 1 ) ); |
|
TakeDamage( info ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Aim the next rocket at a specific target |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::InputFireMissileAt( inputdata_t &inputdata ) |
|
{ |
|
string_t strMissileTarget = MAKE_STRING( inputdata.value.String() ); |
|
CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, strMissileTarget, NULL, inputdata.pActivator, inputdata.pCaller ); |
|
if ( pTarget == NULL ) |
|
{ |
|
DevWarning( "%s: Could not find target '%s'!\n", GetClassname(), STRING( strMissileTarget ) ); |
|
return; |
|
} |
|
|
|
m_hSpecificRocketTarget = pTarget; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int CPropAPC::OnTakeDamage( const CTakeDamageInfo &info ) |
|
{ |
|
if ( m_iHealth == 0 ) |
|
return 0; |
|
|
|
m_OnDamaged.FireOutput( info.GetAttacker(), this ); |
|
|
|
if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() ) |
|
{ |
|
m_OnDamagedByPlayer.FireOutput( info.GetAttacker(), this ); |
|
} |
|
|
|
CTakeDamageInfo dmgInfo = info; |
|
if ( dmgInfo.GetDamageType() & (DMG_BLAST | DMG_AIRBOAT) ) |
|
{ |
|
int nPrevHealth = GetHealth(); |
|
|
|
m_iHealth -= dmgInfo.GetDamage(); |
|
if ( m_iHealth <= 0 ) |
|
{ |
|
m_iHealth = 0; |
|
Event_Killed( dmgInfo ); |
|
return 0; |
|
} |
|
|
|
// Chain |
|
// BaseClass::OnTakeDamage( dmgInfo ); |
|
|
|
// Spawn damage effects |
|
if ( nPrevHealth != GetHealth() ) |
|
{ |
|
if ( ShouldTriggerDamageEffect( nPrevHealth, MAX_SMOKE_TRAILS ) ) |
|
{ |
|
AddSmokeTrail( dmgInfo.GetDamagePosition() ); |
|
} |
|
|
|
if ( ShouldTriggerDamageEffect( nPrevHealth, MAX_EXPLOSIONS ) ) |
|
{ |
|
ExplodeAndThrowChunk( dmgInfo.GetDamagePosition() ); |
|
} |
|
} |
|
} |
|
return 1; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *pMoveData - |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::ProcessMovement( CBasePlayer *pPlayer, CMoveData *pMoveData ) |
|
{ |
|
BaseClass::ProcessMovement( pPlayer, pMoveData ); |
|
|
|
if ( m_flDangerSoundTime > gpGlobals->curtime ) |
|
return; |
|
|
|
QAngle vehicleAngles = GetLocalAngles(); |
|
Vector vecStart = GetAbsOrigin(); |
|
Vector vecDir; |
|
|
|
GetVectors( &vecDir, NULL, NULL ); |
|
|
|
// Make danger sounds ahead of the APC |
|
trace_t tr; |
|
Vector vecSpot, vecLeftDir, vecRightDir; |
|
|
|
// lay down sound path |
|
vecSpot = vecStart + vecDir * 600; |
|
CSoundEnt::InsertSound( SOUND_DANGER, vecSpot, 400, 0.1, this ); |
|
|
|
// put sounds a bit to left and right but slightly closer to APC to make a "cone" of sound |
|
// in front of it |
|
QAngle leftAngles = vehicleAngles; |
|
leftAngles[YAW] += 20; |
|
VehicleAngleVectors( leftAngles, &vecLeftDir, NULL, NULL ); |
|
vecSpot = vecStart + vecLeftDir * 400; |
|
UTIL_TraceLine( vecStart, vecSpot, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); |
|
CSoundEnt::InsertSound( SOUND_DANGER, vecSpot, 400, 0.1, this ); |
|
|
|
QAngle rightAngles = vehicleAngles; |
|
rightAngles[YAW] -= 20; |
|
VehicleAngleVectors( rightAngles, &vecRightDir, NULL, NULL ); |
|
vecSpot = vecStart + vecRightDir * 400; |
|
UTIL_TraceLine( vecStart, vecSpot, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); |
|
CSoundEnt::InsertSound( SOUND_DANGER, vecSpot, 400, 0.1, this); |
|
|
|
m_flDangerSoundTime = gpGlobals->curtime + 0.3; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::Think( void ) |
|
{ |
|
BaseClass::Think(); |
|
|
|
SetNextThink( gpGlobals->curtime ); |
|
|
|
if ( !m_bInitialHandbrake ) // after initial timer expires, set the handbrake |
|
{ |
|
m_bInitialHandbrake = true; |
|
m_VehiclePhysics.SetHandbrake( true ); |
|
m_VehiclePhysics.Think(); |
|
} |
|
|
|
StudioFrameAdvance(); |
|
|
|
if ( IsSequenceFinished() ) |
|
{ |
|
int iSequence = SelectWeightedSequence( ACT_IDLE ); |
|
if ( iSequence > ACTIVITY_NOT_AVAILABLE ) |
|
{ |
|
SetCycle( 0 ); |
|
m_flAnimTime = gpGlobals->curtime; |
|
ResetSequence( iSequence ); |
|
ResetClientsideFrame(); |
|
} |
|
} |
|
|
|
if (m_debugOverlays & OVERLAY_NPC_KILL_BIT) |
|
{ |
|
CTakeDamageInfo info( this, this, m_iHealth, DMG_BLAST ); |
|
info.SetDamagePosition( WorldSpaceCenter() ); |
|
info.SetDamageForce( Vector( 0, 0, 1 ) ); |
|
TakeDamage( info ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Aims the secondary weapon at a target |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::AimSecondaryWeaponAt( CBaseEntity *pTarget ) |
|
{ |
|
m_hRocketTarget = pTarget; |
|
|
|
// Update the rocket target |
|
CreateAPCLaserDot(); |
|
|
|
if ( m_hRocketTarget ) |
|
{ |
|
m_hLaserDot->SetAbsOrigin( m_hRocketTarget->BodyTarget( WorldSpaceCenter(), false ) ); |
|
} |
|
SetLaserDotTarget( m_hLaserDot, m_hRocketTarget ); |
|
EnableLaserDot( m_hLaserDot, m_hRocketTarget != NULL ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::DriveVehicle( float flFrameTime, CUserCmd *ucmd, int iButtonsDown, int iButtonsReleased ) |
|
{ |
|
switch( m_lifeState ) |
|
{ |
|
case LIFE_ALIVE: |
|
{ |
|
int iButtons = ucmd->buttons; |
|
if ( iButtons & IN_ATTACK ) |
|
{ |
|
FireMachineGun(); |
|
} |
|
else if ( iButtons & IN_ATTACK2 ) |
|
{ |
|
FireRocket(); |
|
} |
|
} |
|
break; |
|
|
|
case LIFE_DYING: |
|
FireDying( ); |
|
break; |
|
|
|
case LIFE_DEAD: |
|
return; |
|
} |
|
|
|
BaseClass::DriveVehicle( flFrameTime, ucmd, iButtonsDown, iButtonsReleased ); |
|
} |
|
|
|
void CPropAPC::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) |
|
{ |
|
BaseClass::Use( pActivator, pCaller, useType, value ); |
|
|
|
if ( pActivator->IsPlayer() ) |
|
{ |
|
EmitSound ( "combine.door_lock" ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Primary gun |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::AimPrimaryWeapon( const Vector &vecWorldTarget ) |
|
{ |
|
EntityMatrix parentMatrix; |
|
parentMatrix.InitFromEntity( this, m_nMachineGunBaseAttachment ); |
|
Vector target = parentMatrix.WorldToLocal( vecWorldTarget ); |
|
|
|
float quadTarget = target.LengthSqr(); |
|
float quadTargetXY = target.x*target.x + target.y*target.y; |
|
|
|
// Target is too close! Can't aim at it |
|
if ( quadTarget > m_vecBarrelPos.LengthSqr() ) |
|
{ |
|
// We're trying to aim the offset barrel at an arbitrary point. |
|
// To calculate this, I think of the target as being on a sphere with |
|
// it's center at the origin of the gun. |
|
// The rotation we need is the opposite of the rotation that moves the target |
|
// along the surface of that sphere to intersect with the gun's shooting direction |
|
// To calculate that rotation, we simply calculate the intersection of the ray |
|
// coming out of the barrel with the target sphere (that's the new target position) |
|
// and use atan2() to get angles |
|
|
|
// angles from target pos to center |
|
float targetToCenterYaw = atan2( target.y, target.x ); |
|
float centerToGunYaw = atan2( m_vecBarrelPos.y, sqrt( quadTarget - (m_vecBarrelPos.y*m_vecBarrelPos.y) ) ); |
|
|
|
float targetToCenterPitch = atan2( target.z, sqrt( quadTargetXY ) ); |
|
float centerToGunPitch = atan2( -m_vecBarrelPos.z, sqrt( quadTarget - (m_vecBarrelPos.z*m_vecBarrelPos.z) ) ); |
|
|
|
QAngle angles; |
|
angles.Init( -RAD2DEG(targetToCenterPitch+centerToGunPitch), RAD2DEG( targetToCenterYaw + centerToGunYaw ), 0 ); |
|
|
|
SetPoseParameter( "vehicle_weapon_yaw", angles.y ); |
|
SetPoseParameter( "vehicle_weapon_pitch", angles.x ); |
|
StudioFrameAdvance(); |
|
|
|
float curPitch = GetPoseParameter( "vehicle_weapon_pitch" ); |
|
float curYaw = GetPoseParameter( "vehicle_weapon_yaw" ); |
|
m_bInFiringCone = (fabs(curPitch - angles.x) < 1e-3) && (fabs(curYaw - angles.y) < 1e-3); |
|
} |
|
else |
|
{ |
|
m_bInFiringCone = false; |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
const char *CPropAPC::GetTracerType( void ) |
|
{ |
|
return "HelicopterTracer"; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Allows the shooter to change the impact effect of his bullets |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::DoImpactEffect( trace_t &tr, int nDamageType ) |
|
{ |
|
UTIL_ImpactTrace( &tr, nDamageType, "HelicopterImpact" ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::DoMuzzleFlash( void ) |
|
{ |
|
CEffectData data; |
|
data.m_nEntIndex = entindex(); |
|
data.m_nAttachmentIndex = m_nMachineGunMuzzleAttachment; |
|
data.m_flScale = 1.0f; |
|
DispatchEffect( "ChopperMuzzleFlash", data ); |
|
|
|
BaseClass::DoMuzzleFlash(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::FireMachineGun( void ) |
|
{ |
|
if ( m_flMachineGunTime > gpGlobals->curtime ) |
|
return; |
|
|
|
// If we're still firing the salvo, fire quickly |
|
m_iMachineGunBurstLeft--; |
|
if ( m_iMachineGunBurstLeft > 0 ) |
|
{ |
|
m_flMachineGunTime = gpGlobals->curtime + MACHINE_GUN_BURST_TIME; |
|
} |
|
else |
|
{ |
|
// Reload the salvo |
|
m_iMachineGunBurstLeft = MACHINE_GUN_BURST_SIZE; |
|
m_flMachineGunTime = gpGlobals->curtime + MACHINE_GUN_BURST_PAUSE_TIME; |
|
} |
|
|
|
Vector vecMachineGunShootPos; |
|
Vector vecMachineGunDir; |
|
GetAttachment( m_nMachineGunMuzzleAttachment, vecMachineGunShootPos, &vecMachineGunDir ); |
|
|
|
// Fire the round |
|
int bulletType = GetAmmoDef()->Index("AR2"); |
|
FireBullets( 1, vecMachineGunShootPos, vecMachineGunDir, VECTOR_CONE_8DEGREES, MAX_TRACE_LENGTH, bulletType, 1 ); |
|
DoMuzzleFlash(); |
|
|
|
EmitSound( "Weapon_AR2.Single" ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::GetRocketShootPosition( Vector *pPosition ) |
|
{ |
|
GetAttachment( m_nRocketAttachment, *pPosition ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Create a corpse |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::CreateCorpse( ) |
|
{ |
|
m_lifeState = LIFE_DEAD; |
|
|
|
for ( int i = 0; i < APC_MAX_GIBS; ++i ) |
|
{ |
|
CPhysicsProp *pGib = assert_cast<CPhysicsProp*>(CreateEntityByName( "prop_physics" )); |
|
pGib->SetAbsOrigin( GetAbsOrigin() ); |
|
pGib->SetAbsAngles( GetAbsAngles() ); |
|
pGib->SetAbsVelocity( GetAbsVelocity() ); |
|
pGib->SetModel( s_pGibModelName[i] ); |
|
pGib->Spawn(); |
|
pGib->SetMoveType( MOVETYPE_VPHYSICS ); |
|
|
|
float flMass = pGib->GetMass(); |
|
if ( flMass < 200 ) |
|
{ |
|
Vector vecVelocity; |
|
pGib->GetMassCenter( &vecVelocity ); |
|
vecVelocity -= WorldSpaceCenter(); |
|
vecVelocity.z = fabs(vecVelocity.z); |
|
VectorNormalize( vecVelocity ); |
|
|
|
// Apply a force that would make a 100kg mass travel 150 - 300 m/s |
|
float flRandomVel = random->RandomFloat( 150, 300 ); |
|
vecVelocity *= (100 * flRandomVel) / flMass; |
|
vecVelocity.z += 100.0f; |
|
AngularImpulse angImpulse = RandomAngularImpulse( -500, 500 ); |
|
|
|
IPhysicsObject *pObj = pGib->VPhysicsGetObject(); |
|
if ( pObj != NULL ) |
|
{ |
|
pObj->AddVelocity( &vecVelocity, &angImpulse ); |
|
} |
|
pGib->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); |
|
} |
|
if( hl2_episodic.GetBool() ) |
|
{ |
|
// EP1 perf hit |
|
pGib->Ignite( 6, false ); |
|
} |
|
else |
|
{ |
|
pGib->Ignite( 60, false ); |
|
} |
|
} |
|
|
|
AddSolidFlags( FSOLID_NOT_SOLID ); |
|
AddEffects( EF_NODRAW ); |
|
UTIL_Remove( this ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Death volley |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::FireDying( ) |
|
{ |
|
if ( m_flRocketTime > gpGlobals->curtime ) |
|
return; |
|
|
|
Vector vecRocketOrigin; |
|
GetRocketShootPosition( &vecRocketOrigin ); |
|
|
|
Vector vecDir; |
|
vecDir.Random( -1.0f, 1.0f ); |
|
if ( vecDir.z < 0.0f ) |
|
{ |
|
vecDir.z *= -1.0f; |
|
} |
|
|
|
VectorNormalize( vecDir ); |
|
|
|
Vector vecVelocity; |
|
VectorMultiply( vecDir, ROCKET_SPEED * random->RandomFloat( 0.75f, 1.25f ), vecVelocity ); |
|
|
|
QAngle angles; |
|
VectorAngles( vecDir, angles ); |
|
|
|
CAPCMissile *pRocket = (CAPCMissile *) CAPCMissile::Create( vecRocketOrigin, angles, vecVelocity, this ); |
|
float flDeathTime = random->RandomFloat( 0.3f, 0.5f ); |
|
if ( random->RandomFloat( 0.0f, 1.0f ) < 0.3f ) |
|
{ |
|
pRocket->ExplodeDelay( flDeathTime ); |
|
} |
|
else |
|
{ |
|
pRocket->AugerDelay( flDeathTime ); |
|
} |
|
|
|
// Make erratic firing |
|
m_flRocketTime = gpGlobals->curtime + random->RandomFloat( DEATH_VOLLEY_MIN_FIRE_TIME, DEATH_VOLLEY_MAX_FIRE_TIME ); |
|
if ( --m_iRocketSalvoLeft <= 0 ) |
|
{ |
|
CreateCorpse(); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::FireRocket( void ) |
|
{ |
|
if ( m_flRocketTime > gpGlobals->curtime ) |
|
return; |
|
|
|
// If we're still firing the salvo, fire quickly |
|
m_iRocketSalvoLeft--; |
|
if ( m_iRocketSalvoLeft > 0 ) |
|
{ |
|
m_flRocketTime = gpGlobals->curtime + ROCKET_DELAY_TIME; |
|
} |
|
else |
|
{ |
|
// Reload the salvo |
|
m_iRocketSalvoLeft = ROCKET_SALVO_SIZE; |
|
m_flRocketTime = gpGlobals->curtime + random->RandomFloat( ROCKET_MIN_BURST_PAUSE_TIME, ROCKET_MAX_BURST_PAUSE_TIME ); |
|
} |
|
|
|
Vector vecRocketOrigin; |
|
GetRocketShootPosition( &vecRocketOrigin ); |
|
|
|
static float s_pSide[] = { 0.966, 0.866, 0.5, -0.5, -0.866, -0.966 }; |
|
|
|
Vector forward; |
|
GetVectors( &forward, NULL, NULL ); |
|
|
|
Vector vecDir; |
|
CrossProduct( Vector( 0, 0, 1 ), forward, vecDir ); |
|
vecDir.z = 1.0f; |
|
vecDir.x *= s_pSide[m_nRocketSide]; |
|
vecDir.y *= s_pSide[m_nRocketSide]; |
|
if ( ++m_nRocketSide >= 6 ) |
|
{ |
|
m_nRocketSide = 0; |
|
} |
|
|
|
VectorNormalize( vecDir ); |
|
|
|
Vector vecVelocity; |
|
VectorMultiply( vecDir, ROCKET_SPEED, vecVelocity ); |
|
|
|
QAngle angles; |
|
VectorAngles( vecDir, angles ); |
|
|
|
CAPCMissile *pRocket = (CAPCMissile *)CAPCMissile::Create( vecRocketOrigin, angles, vecVelocity, this ); |
|
pRocket->IgniteDelay(); |
|
|
|
if ( m_hSpecificRocketTarget ) |
|
{ |
|
pRocket->AimAtSpecificTarget( m_hSpecificRocketTarget ); |
|
m_hSpecificRocketTarget = NULL; |
|
} |
|
else if ( m_strMissileHint != NULL_STRING ) |
|
{ |
|
pRocket->SetGuidanceHint( STRING( m_strMissileHint ) ); |
|
} |
|
|
|
EmitSound( "PropAPC.FireRocket" ); |
|
m_OnFiredMissile.FireOutput( this, this ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
float CPropAPC::MaxAttackRange() const |
|
{ |
|
return ROCKET_ATTACK_RANGE_MAX; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPropAPC::OnRestore( void ) |
|
{ |
|
IServerVehicle *pServerVehicle = GetServerVehicle(); |
|
if ( pServerVehicle != NULL ) |
|
{ |
|
// Restore the passenger information we're holding on to |
|
pServerVehicle->RestorePassengerInfo(); |
|
} |
|
} |
|
|
|
//======================================================================================================================================== |
|
// APC FOUR WHEEL PHYSICS VEHICLE SERVER VEHICLE |
|
//======================================================================================================================================== |
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAPCFourWheelServerVehicle::NPC_AimPrimaryWeapon( Vector vecTarget ) |
|
{ |
|
CPropAPC *pAPC = ((CPropAPC*)m_pVehicle); |
|
pAPC->AimPrimaryWeapon( vecTarget ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAPCFourWheelServerVehicle::NPC_AimSecondaryWeapon( Vector vecTarget ) |
|
{ |
|
// Add some random noise |
|
// Vector vecOffset = vecTarget + RandomVector( -128, 128 ); |
|
// ((CPropAPC*)m_pVehicle)->AimSecondaryWeaponAt( vecOffset ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAPCFourWheelServerVehicle::Weapon_PrimaryRanges( float *flMinRange, float *flMaxRange ) |
|
{ |
|
*flMinRange = MACHINE_GUN_ATTACK_RANGE_MIN; |
|
*flMaxRange = MACHINE_GUN_ATTACK_RANGE_MAX; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAPCFourWheelServerVehicle::Weapon_SecondaryRanges( float *flMinRange, float *flMaxRange ) |
|
{ |
|
*flMinRange = ROCKET_ATTACK_RANGE_MIN; |
|
*flMaxRange = ROCKET_ATTACK_RANGE_MAX; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Return the time at which this vehicle's primary weapon can fire again |
|
//----------------------------------------------------------------------------- |
|
float CAPCFourWheelServerVehicle::Weapon_PrimaryCanFireAt( void ) |
|
{ |
|
return ((CPropAPC*)m_pVehicle)->PrimaryWeaponFireTime(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Return the time at which this vehicle's secondary weapon can fire again |
|
//----------------------------------------------------------------------------- |
|
float CAPCFourWheelServerVehicle::Weapon_SecondaryCanFireAt( void ) |
|
{ |
|
return ((CPropAPC*)m_pVehicle)->SecondaryWeaponFireTime(); |
|
} |
|
|
|
|