source-engine/game/shared/tf2base/tf_weapon_flamethrower.cpp
2022-08-10 19:52:28 +03:00

973 lines
30 KiB
C++

//====== Copyright © 1996-2005, Valve Corporation, All rights reserved. =======
//
// TF Flame Thrower
//
//=============================================================================
#include "cbase.h"
#include "tf_weapon_flamethrower.h"
#include "tf_fx_shared.h"
#include "in_buttons.h"
#include "ammodef.h"
#if defined( CLIENT_DLL )
#include "c_tf_player.h"
#include "vstdlib/random.h"
#include "engine/IEngineSound.h"
#include "soundenvelope.h"
#else
#include "explode.h"
#include "tf_player.h"
#include "tf_gamerules.h"
#include "tf_gamestats.h"
#include "ilagcompensationmanager.h"
#include "collisionutils.h"
#include "tf_team.h"
#include "tf_obj.h"
ConVar tf_debug_flamethrower("tf_debug_flamethrower", "0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Visualize the flamethrower damage." );
ConVar tf_flamethrower_velocity( "tf_flamethrower_velocity", "2300.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Initial velocity of flame damage entities." );
ConVar tf_flamethrower_drag("tf_flamethrower_drag", "0.89", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Air drag of flame damage entities." );
ConVar tf_flamethrower_float("tf_flamethrower_float", "50.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Upward float velocity of flame damage entities." );
ConVar tf_flamethrower_flametime("tf_flamethrower_flametime", "0.5", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Time to live of flame damage entities." );
ConVar tf_flamethrower_vecrand("tf_flamethrower_vecrand", "0.05", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Random vector added to initial velocity of flame damage entities." );
ConVar tf_flamethrower_boxsize("tf_flamethrower_boxsize", "8.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Size of flame damage entities." );
ConVar tf_flamethrower_maxdamagedist("tf_flamethrower_maxdamagedist", "350.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Maximum damage distance for flamethrower." );
ConVar tf_flamethrower_shortrangedamagemultiplier("tf_flamethrower_shortrangedamagemultiplier", "1.2", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Damage multiplier for close-in flamethrower damage." );
ConVar tf_flamethrower_velocityfadestart("tf_flamethrower_velocityfadestart", ".3", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Time at which attacker's velocity contribution starts to fade." );
ConVar tf_flamethrower_velocityfadeend("tf_flamethrower_velocityfadeend", ".5", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Time at which attacker's velocity contribution finishes fading." );
//ConVar tf_flame_force( "tf_flame_force", "30" );
#endif
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
// position of end of muzzle relative to shoot position
#define TF_FLAMETHROWER_MUZZLEPOS_FORWARD 70.0f
#define TF_FLAMETHROWER_MUZZLEPOS_RIGHT 12.0f
#define TF_FLAMETHROWER_MUZZLEPOS_UP -12.0f
#define TF_FLAMETHROWER_AMMO_PER_SECOND_PRIMARY_ATTACK 14.0f
#define TF_FLAMETHROWER_AMMO_PER_SECONDARY_ATTACK 10
IMPLEMENT_NETWORKCLASS_ALIASED( TFFlameThrower, DT_WeaponFlameThrower )
BEGIN_NETWORK_TABLE( CTFFlameThrower, DT_WeaponFlameThrower )
#if defined( CLIENT_DLL )
RecvPropInt( RECVINFO( m_iWeaponState ) ),
RecvPropBool( RECVINFO( m_bCritFire ) )
#else
SendPropInt( SENDINFO( m_iWeaponState ), 4, SPROP_UNSIGNED | SPROP_CHANGES_OFTEN ),
SendPropBool( SENDINFO( m_bCritFire ) )
#endif
END_NETWORK_TABLE()
#if defined( CLIENT_DLL )
BEGIN_PREDICTION_DATA( CTFFlameThrower )
DEFINE_PRED_FIELD( m_iWeaponState, FIELD_INTEGER, FTYPEDESC_INSENDTABLE ),
DEFINE_PRED_FIELD( m_bCritFire, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE ),
END_PREDICTION_DATA()
#endif
LINK_ENTITY_TO_CLASS( tf_weapon_flamethrower, CTFFlameThrower );
PRECACHE_WEAPON_REGISTER( tf_weapon_flamethrower );
BEGIN_DATADESC( CTFFlameThrower )
END_DATADESC()
// ------------------------------------------------------------------------------------------------ //
// CTFFlameThrower implementation.
// ------------------------------------------------------------------------------------------------ //
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CTFFlameThrower::CTFFlameThrower()
{
WeaponReset();
#if defined( CLIENT_DLL )
m_pFiringStartSound = NULL;
m_pFiringLoop = NULL;
m_bFiringLoopCritical = false;
m_pPilotLightSound = NULL;
#endif
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CTFFlameThrower::~CTFFlameThrower()
{
DestroySounds();
}
void CTFFlameThrower::DestroySounds( void )
{
#if defined( CLIENT_DLL )
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController();
if ( m_pFiringStartSound )
{
controller.SoundDestroy( m_pFiringStartSound );
m_pFiringStartSound = NULL;
}
if ( m_pFiringLoop )
{
controller.SoundDestroy( m_pFiringLoop );
m_pFiringLoop = NULL;
}
if ( m_pPilotLightSound )
{
controller.SoundDestroy( m_pPilotLightSound );
m_pPilotLightSound = NULL;
}
#endif
}
void CTFFlameThrower::WeaponReset( void )
{
BaseClass::WeaponReset();
m_iWeaponState = FT_STATE_IDLE;
m_bCritFire = false;
m_flStartFiringTime = 0;
m_flAmmoUseRemainder = 0;
DestroySounds();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::Spawn( void )
{
m_iAltFireHint = HINT_ALTFIRE_FLAMETHROWER;
BaseClass::Spawn();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CTFFlameThrower::Holster( CBaseCombatWeapon *pSwitchingTo )
{
m_iWeaponState = FT_STATE_IDLE;
m_bCritFire = false;
#if defined ( CLIENT_DLL )
StopFlame();
StopPilotLight();
#endif
return BaseClass::Holster( pSwitchingTo );
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::ItemPostFrame()
{
if ( m_bLowered )
return;
// Get the player owning the weapon.
CTFPlayer *pOwner = ToTFPlayer( GetPlayerOwner() );
if ( !pOwner )
return;
int iAmmo = pOwner->GetAmmoCount( m_iPrimaryAmmoType );
if ( pOwner->IsAlive() && ( pOwner->m_nButtons & IN_ATTACK ) && iAmmo > 0 )
{
PrimaryAttack();
}
else if ( m_iWeaponState > FT_STATE_IDLE )
{
SendWeaponAnim( ACT_MP_ATTACK_STAND_POSTFIRE );
pOwner->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_POST );
m_iWeaponState = FT_STATE_IDLE;
m_bCritFire = false;
}
if ( pOwner->IsAlive() && ( pOwner->m_nButtons & IN_ATTACK2 ) && iAmmo > TF_FLAMETHROWER_AMMO_PER_SECONDARY_ATTACK )
{
SecondaryAttack();
}
BaseClass::ItemPostFrame();
}
class CTraceFilterIgnoreObjects : public CTraceFilterSimple
{
public:
// It does have a base, but we'll never network anything below here..
DECLARE_CLASS( CTraceFilterIgnoreObjects, CTraceFilterSimple );
CTraceFilterIgnoreObjects( const IHandleEntity *passentity, int collisionGroup )
: CTraceFilterSimple( passentity, collisionGroup )
{
}
virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask )
{
CBaseEntity *pEntity = EntityFromEntityHandle( pServerEntity );
if ( pEntity && pEntity->IsBaseObject() )
return false;
return BaseClass::ShouldHitEntity( pServerEntity, contentsMask );
}
};
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::PrimaryAttack()
{
// Are we capable of firing again?
if ( m_flNextPrimaryAttack > gpGlobals->curtime )
return;
// Get the player owning the weapon.
CTFPlayer *pOwner = ToTFPlayer( GetPlayerOwner() );
if ( !pOwner )
return;
if ( !CanAttack() )
{
#if defined ( CLIENT_DLL )
StopFlame();
#endif
m_iWeaponState = FT_STATE_IDLE;
return;
}
CalcIsAttackCritical();
// Because the muzzle is so long, it can stick through a wall if the player is right up against it.
// Make sure the weapon can't fire in this condition by tracing a line between the eye point and the end of the muzzle.
trace_t trace;
Vector vecEye = pOwner->EyePosition();
Vector vecMuzzlePos = GetVisualMuzzlePos();
CTraceFilterIgnoreObjects traceFilter( this, COLLISION_GROUP_NONE );
UTIL_TraceLine( vecEye, vecMuzzlePos, MASK_SOLID, &traceFilter, &trace );
if ( trace.fraction < 1.0 && ( !trace.m_pEnt || trace.m_pEnt->m_takedamage == DAMAGE_NO ) )
{
// there is something between the eye and the end of the muzzle, most likely a wall, don't fire, and stop firing if we already are
if ( m_iWeaponState > FT_STATE_IDLE )
{
#if defined ( CLIENT_DLL )
StopFlame();
#endif
m_iWeaponState = FT_STATE_IDLE;
}
return;
}
switch ( m_iWeaponState )
{
case FT_STATE_IDLE:
{
// Just started, play PRE and start looping view model anim
pOwner->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRE );
SendWeaponAnim( ACT_VM_PRIMARYATTACK );
m_flStartFiringTime = gpGlobals->curtime + 0.16; // 5 frames at 30 fps
m_iWeaponState = FT_STATE_STARTFIRING;
}
break;
case FT_STATE_STARTFIRING:
{
// if some time has elapsed, start playing the looping third person anim
if ( gpGlobals->curtime > m_flStartFiringTime )
{
m_iWeaponState = FT_STATE_FIRING;
m_flNextPrimaryAttackAnim = gpGlobals->curtime;
}
}
break;
case FT_STATE_FIRING:
{
if ( gpGlobals->curtime >= m_flNextPrimaryAttackAnim )
{
pOwner->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRIMARY );
m_flNextPrimaryAttackAnim = gpGlobals->curtime + 1.4; // fewer than 45 frames!
}
}
break;
default:
break;
}
#ifdef CLIENT_DLL
// Restart our particle effect if we've transitioned across water boundaries
if ( m_iParticleWaterLevel != -1 && pOwner->GetWaterLevel() != m_iParticleWaterLevel )
{
if ( m_iParticleWaterLevel == WL_Eyes || pOwner->GetWaterLevel() == WL_Eyes )
{
RestartParticleEffect();
}
}
#endif
#if !defined (CLIENT_DLL)
// Let the player remember the usercmd he fired a weapon on. Assists in making decisions about lag compensation.
pOwner->NoteWeaponFired();
pOwner->SpeakWeaponFire();
CTF_GameStats.Event_PlayerFiredWeapon( pOwner, m_bCritFire );
// Move other players back to history positions based on local player's lag
lagcompensation->StartLagCompensation( pOwner, pOwner->GetCurrentCommand() );
#endif
float flFiringInterval = m_pWeaponInfo->GetWeaponData( m_iWeaponMode ).m_flTimeFireDelay;
// Don't attack if we're underwater
if ( pOwner->GetWaterLevel() != WL_Eyes )
{
// Find eligible entities in a cone in front of us.
Vector vOrigin = pOwner->Weapon_ShootPosition();
Vector vForward, vRight, vUp;
QAngle vAngles = pOwner->EyeAngles() + pOwner->GetPunchAngle();
AngleVectors( vAngles, &vForward, &vRight, &vUp );
#define NUM_TEST_VECTORS 30
#ifdef CLIENT_DLL
bool bWasCritical = m_bCritFire;
#endif
// Burn & Ignite 'em
int iDmgType = g_aWeaponDamageTypes[ GetWeaponID() ];
m_bCritFire = IsCurrentAttackACrit();
if ( m_bCritFire )
{
iDmgType |= DMG_CRITICAL;
}
#ifdef CLIENT_DLL
if ( bWasCritical != m_bCritFire )
{
RestartParticleEffect();
}
#endif
#ifdef GAME_DLL
// create the flame entity
int iDamagePerSec = m_pWeaponInfo->GetWeaponData( m_iWeaponMode ).m_nDamage;
float flDamage = (float)iDamagePerSec * flFiringInterval;
CTFFlameEntity::Create( GetFlameOriginPos(), pOwner->EyeAngles(), this, iDmgType, flDamage );
#endif
}
#ifdef GAME_DLL
// Figure how much ammo we're using per shot and add it to our remainder to subtract. (We may be using less than 1.0 ammo units
// per frame, depending on how constants are tuned, so keep an accumulator so we can expend fractional amounts of ammo per shot.)
// Note we do this only on server and network it to client. If we predict it on client, it can get slightly out of sync w/server
// and cause ammo pickup indicators to appear
m_flAmmoUseRemainder += TF_FLAMETHROWER_AMMO_PER_SECOND_PRIMARY_ATTACK * flFiringInterval;
// take the integer portion of the ammo use accumulator and subtract it from player's ammo count; any fractional amount of ammo use
// remains and will get used in the next shot
int iAmmoToSubtract = (int) m_flAmmoUseRemainder;
if ( iAmmoToSubtract > 0 )
{
pOwner->RemoveAmmo( iAmmoToSubtract, m_iPrimaryAmmoType );
m_flAmmoUseRemainder -= iAmmoToSubtract;
// round to 2 digits of precision
m_flAmmoUseRemainder = (float) ( (int) (m_flAmmoUseRemainder * 100) ) / 100.0f;
}
#endif
m_flNextPrimaryAttack = gpGlobals->curtime + flFiringInterval;
m_flTimeWeaponIdle = gpGlobals->curtime + flFiringInterval;
#if !defined (CLIENT_DLL)
lagcompensation->FinishLagCompensation( pOwner );
#endif
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::SecondaryAttack()
{
// Disabled until we know what this will do
return;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CTFFlameThrower::Lower( void )
{
if ( BaseClass::Lower() )
{
// If we were firing, stop
if ( m_iWeaponState > FT_STATE_IDLE )
{
SendWeaponAnim( ACT_MP_ATTACK_STAND_POSTFIRE );
m_iWeaponState = FT_STATE_IDLE;
}
return true;
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose: Returns the position of the tip of the muzzle at it appears visually
//-----------------------------------------------------------------------------
Vector CTFFlameThrower::GetVisualMuzzlePos()
{
return GetMuzzlePosHelper( true );
}
//-----------------------------------------------------------------------------
// Purpose: Returns the position at which to spawn flame damage entities
//-----------------------------------------------------------------------------
Vector CTFFlameThrower::GetFlameOriginPos()
{
return GetMuzzlePosHelper( false );
}
//-----------------------------------------------------------------------------
// Purpose: Returns the position of the tip of the muzzle
//-----------------------------------------------------------------------------
Vector CTFFlameThrower::GetMuzzlePosHelper( bool bVisualPos )
{
Vector vecMuzzlePos;
CTFPlayer *pOwner = ToTFPlayer( GetPlayerOwner() );
if ( pOwner )
{
Vector vecForward, vecRight, vecUp;
AngleVectors( pOwner->GetAbsAngles(), &vecForward, &vecRight, &vecUp );
vecMuzzlePos = pOwner->Weapon_ShootPosition();
vecMuzzlePos += vecRight * TF_FLAMETHROWER_MUZZLEPOS_RIGHT;
// if asking for visual position of muzzle, include the forward component
if ( bVisualPos )
{
vecMuzzlePos += vecForward * TF_FLAMETHROWER_MUZZLEPOS_FORWARD;
}
}
return vecMuzzlePos;
}
#if defined( CLIENT_DLL )
bool CTFFlameThrower::Deploy( void )
{
StartPilotLight();
return BaseClass::Deploy();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::OnDataChanged(DataUpdateType_t updateType)
{
BaseClass::OnDataChanged(updateType);
if ( IsCarrierAlive() && ( WeaponState() == WEAPON_IS_ACTIVE ) && ( GetPlayerOwner()->GetAmmoCount( m_iPrimaryAmmoType ) > 0 ) )
{
if ( m_iWeaponState > FT_STATE_IDLE )
{
StartFlame();
}
else
{
StartPilotLight();
}
}
else
{
StopFlame();
StopPilotLight();
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::UpdateOnRemove( void )
{
StopFlame();
StopPilotLight();
BaseClass::UpdateOnRemove();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::SetDormant( bool bDormant )
{
// If I'm going from active to dormant and I'm carried by another player, stop our firing sound.
if ( !IsCarriedByLocalPlayer() )
{
if ( !IsDormant() && bDormant )
{
StopFlame();
StopPilotLight();
}
}
// Deliberately skip base combat weapon to avoid being holstered
C_BaseEntity::SetDormant( bDormant );
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::StartFlame()
{
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController();
// normally, crossfade between start sound & firing loop in 3.5 sec
float flCrossfadeTime = 3.5;
if ( m_pFiringLoop && ( m_bCritFire != m_bFiringLoopCritical ) )
{
// If we're firing and changing between critical & noncritical, just need to change the firing loop.
// Set crossfade time to zero so we skip the start sound and go to the loop immediately.
flCrossfadeTime = 0;
StopFlame( true );
}
StopPilotLight();
if ( !m_pFiringStartSound && !m_pFiringLoop )
{
RestartParticleEffect();
CLocalPlayerFilter filter;
// Play the fire start sound
const char *shootsound = GetShootSound( SINGLE );
if ( flCrossfadeTime > 0.0 )
{
// play the firing start sound and fade it out
m_pFiringStartSound = controller.SoundCreate( filter, entindex(), shootsound );
controller.Play( m_pFiringStartSound, 1.0, 100 );
controller.SoundChangeVolume( m_pFiringStartSound, 0.0, flCrossfadeTime );
}
// Start the fire sound loop and fade it in
if ( m_bCritFire )
{
shootsound = GetShootSound( BURST );
}
else
{
shootsound = GetShootSound( SPECIAL1 );
}
m_pFiringLoop = controller.SoundCreate( filter, entindex(), shootsound );
m_bFiringLoopCritical = m_bCritFire;
// play the firing loop sound and fade it in
if ( flCrossfadeTime > 0.0 )
{
controller.Play( m_pFiringLoop, 0.0, 100 );
controller.SoundChangeVolume( m_pFiringLoop, 1.0, flCrossfadeTime );
}
else
{
controller.Play( m_pFiringLoop, 1.0, 100 );
}
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::StopFlame( bool bAbrupt /* = false */ )
{
if ( ( m_pFiringLoop || m_pFiringStartSound ) && !bAbrupt )
{
// play a quick wind-down poof when the flame stops
CLocalPlayerFilter filter;
const char *shootsound = GetShootSound( SPECIAL3 );
EmitSound( filter, entindex(), shootsound );
}
if ( m_pFiringLoop )
{
CSoundEnvelopeController::GetController().SoundDestroy( m_pFiringLoop );
m_pFiringLoop = NULL;
}
if ( m_pFiringStartSound )
{
CSoundEnvelopeController::GetController().SoundDestroy( m_pFiringStartSound );
m_pFiringStartSound = NULL;
}
if ( m_bFlameEffects )
{
// Stop the effect on the viewmodel if our owner is the local player
C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer();
if ( pLocalPlayer && pLocalPlayer == GetOwner() )
{
if ( pLocalPlayer->GetViewModel() )
{
pLocalPlayer->GetViewModel()->ParticleProp()->StopEmission();
}
}
else
{
ParticleProp()->StopEmission();
}
}
m_bFlameEffects = false;
m_iParticleWaterLevel = -1;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::StartPilotLight()
{
if ( !m_pPilotLightSound )
{
StopFlame();
// Create the looping pilot light sound
const char *pilotlightsound = GetShootSound( SPECIAL2 );
CLocalPlayerFilter filter;
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController();
m_pPilotLightSound = controller.SoundCreate( filter, entindex(), pilotlightsound );
controller.Play( m_pPilotLightSound, 1.0, 100 );
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::StopPilotLight()
{
if ( m_pPilotLightSound )
{
CSoundEnvelopeController::GetController().SoundDestroy( m_pPilotLightSound );
m_pPilotLightSound = NULL;
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameThrower::RestartParticleEffect( void )
{
CTFPlayer *pOwner = ToTFPlayer( GetPlayerOwner() );
if ( !pOwner )
return;
m_iParticleWaterLevel = pOwner->GetWaterLevel();
// Start the appropriate particle effect
const char *pszParticleEffect;
if ( pOwner->GetWaterLevel() == WL_Eyes )
{
pszParticleEffect = "flamethrower_underwater";
}
else
{
if ( m_bCritFire )
{
pszParticleEffect = ( pOwner->GetTeamNumber() == TF_TEAM_BLUE ? "flamethrower_crit_blue" : "flamethrower_crit_red" );
}
else
{
pszParticleEffect = ( pOwner->GetTeamNumber() == TF_TEAM_BLUE ? "flamethrower_blue" : "flamethrower" );
}
}
// Start the effect on the viewmodel if our owner is the local player
C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer();
if ( pLocalPlayer && pLocalPlayer == GetOwner() )
{
if ( pLocalPlayer->GetViewModel() )
{
pLocalPlayer->GetViewModel()->ParticleProp()->StopEmission();
pLocalPlayer->GetViewModel()->ParticleProp()->Create( pszParticleEffect, PATTACH_POINT_FOLLOW, "muzzle" );
}
}
else
{
ParticleProp()->StopEmission();
ParticleProp()->Create( pszParticleEffect, PATTACH_POINT_FOLLOW, "muzzle" );
}
m_bFlameEffects = true;
}
#endif
#ifdef GAME_DLL
LINK_ENTITY_TO_CLASS( tf_flame, CTFFlameEntity );
//-----------------------------------------------------------------------------
// Purpose: Spawns this entitye
//-----------------------------------------------------------------------------
void CTFFlameEntity::Spawn( void )
{
BaseClass::Spawn();
// don't collide with anything, we do our own collision detection in our think method
SetSolid( SOLID_NONE );
SetSolidFlags( FSOLID_NOT_SOLID );
SetCollisionGroup( COLLISION_GROUP_NONE );
// move noclip: update position from velocity, that's it
SetMoveType( MOVETYPE_NOCLIP, MOVECOLLIDE_DEFAULT );
AddEFlags( EFL_NO_WATER_VELOCITY_CHANGE );
float iBoxSize = tf_flamethrower_boxsize.GetFloat();
UTIL_SetSize( this, -Vector( iBoxSize, iBoxSize, iBoxSize ), Vector( iBoxSize, iBoxSize, iBoxSize ) );
// Setup attributes.
m_takedamage = DAMAGE_NO;
m_vecInitialPos = GetAbsOrigin();
m_vecPrevPos = m_vecInitialPos;
m_flTimeRemove = gpGlobals->curtime + ( tf_flamethrower_flametime.GetFloat() * random->RandomFloat( 0.9, 1.1 ) );
// Setup the think function.
SetThink( &CTFFlameEntity::FlameThink );
SetNextThink( gpGlobals->curtime );
}
//-----------------------------------------------------------------------------
// Purpose: Creates an instance of this entity
//-----------------------------------------------------------------------------
CTFFlameEntity *CTFFlameEntity::Create( const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity *pOwner, int iDmgType, float flDmgAmount )
{
CTFFlameEntity *pFlame = static_cast<CTFFlameEntity*>( CBaseEntity::Create( "tf_flame", vecOrigin, vecAngles, pOwner ) );
if ( !pFlame )
return NULL;
// Initialize the owner.
pFlame->SetOwnerEntity( pOwner );
pFlame->m_hAttacker = pOwner->GetOwnerEntity();
CBaseEntity *pAttacker = (CBaseEntity *) pFlame->m_hAttacker;
if ( pAttacker )
{
pFlame->m_iAttackerTeam = pAttacker->GetTeamNumber();
}
// Set team.
pFlame->ChangeTeam( pOwner->GetTeamNumber() );
pFlame->m_iDmgType = iDmgType;
pFlame->m_flDmgAmount = flDmgAmount;
// Setup the initial velocity.
Vector vecForward, vecRight, vecUp;
AngleVectors( vecAngles, &vecForward, &vecRight, &vecUp );
float velocity = tf_flamethrower_velocity.GetFloat();
pFlame->m_vecBaseVelocity = vecForward * velocity;
pFlame->m_vecBaseVelocity += RandomVector( -velocity * tf_flamethrower_vecrand.GetFloat(), velocity * tf_flamethrower_vecrand.GetFloat() );
pFlame->m_vecAttackerVelocity = pOwner->GetOwnerEntity()->GetAbsVelocity();
pFlame->SetAbsVelocity( pFlame->m_vecBaseVelocity );
// Setup the initial angles.
pFlame->SetAbsAngles( vecAngles );
return pFlame;
}
//-----------------------------------------------------------------------------
// Purpose: Think method
//-----------------------------------------------------------------------------
void CTFFlameEntity::FlameThink( void )
{
// if we've expired, remove ourselves
if ( gpGlobals->curtime >= m_flTimeRemove )
{
UTIL_Remove( this );
return;
}
// Do collision detection. We do custom collision detection because we can do it more cheaply than the
// standard collision detection (don't need to check against world unless we might have hit an enemy) and
// flame entity collision detection w/o this was a bottleneck on the X360 server
if ( GetAbsOrigin() != m_vecPrevPos )
{
CTFPlayer *pAttacker = dynamic_cast<CTFPlayer *>( (CBaseEntity *) m_hAttacker );
if ( !pAttacker )
return;
CTFTeam *pTeam = pAttacker->GetOpposingTFTeam();
if ( !pTeam )
return;
bool bHitWorld = false;
// check collision against all enemy players
for ( int iPlayer= 0; iPlayer < pTeam->GetNumPlayers(); iPlayer++ )
{
CBasePlayer *pPlayer = pTeam->GetPlayer( iPlayer );
// Is this player connected, alive, and an enemy?
if ( pPlayer && pPlayer->IsConnected() && pPlayer->IsAlive() )
{
CheckCollision( pPlayer, &bHitWorld );
if ( bHitWorld )
return;
}
}
// check collision against all enemy objects
for ( int iObject = 0; iObject < pTeam->GetNumObjects(); iObject++ )
{
CBaseObject *pObject = pTeam->GetObject( iObject );
if ( pObject )
{
CheckCollision( pObject, &bHitWorld );
if ( bHitWorld )
return;
}
}
}
// Calculate how long the flame has been alive for
float flFlameElapsedTime = tf_flamethrower_flametime.GetFloat() - ( m_flTimeRemove - gpGlobals->curtime );
// Calculate how much of the attacker's velocity to blend in to the flame's velocity. The flame gets the attacker's velocity
// added right when the flame is fired, but that velocity addition fades quickly to zero.
float flAttackerVelocityBlend = RemapValClamped( flFlameElapsedTime, tf_flamethrower_velocityfadestart.GetFloat(),
tf_flamethrower_velocityfadeend.GetFloat(), 1.0, 0 );
// Reduce our base velocity by the air drag constant
m_vecBaseVelocity *= tf_flamethrower_drag.GetFloat();
// Add our float upward velocity
Vector vecVelocity = m_vecBaseVelocity + Vector( 0, 0, tf_flamethrower_float.GetFloat() ) + ( flAttackerVelocityBlend * m_vecAttackerVelocity );
// Update our velocity
SetAbsVelocity( vecVelocity );
// Render debug visualization if convar on
if ( tf_debug_flamethrower.GetInt() )
{
if ( m_hEntitiesBurnt.Count() > 0 )
{
int val = ( (int) ( gpGlobals->curtime * 10 ) ) % 255;
NDebugOverlay::EntityBounds(this, val, 255, val, 0 ,0 );
}
else
{
NDebugOverlay::EntityBounds(this, 0, 100, 255, 0 ,0) ;
}
}
SetNextThink( gpGlobals->curtime );
m_vecPrevPos = GetAbsOrigin();
}
//-----------------------------------------------------------------------------
// Purpose: Checks collisions against other entities
//-----------------------------------------------------------------------------
void CTFFlameEntity::CheckCollision( CBaseEntity *pOther, bool *pbHitWorld )
{
*pbHitWorld = false;
// if we've already burnt this entity, don't do more damage, so skip even checking for collision with the entity
int iIndex = m_hEntitiesBurnt.Find( pOther );
if ( iIndex != m_hEntitiesBurnt.InvalidIndex() )
return;
// Do a bounding box check against the entity
Vector vecMins, vecMaxs;
pOther->GetCollideable()->WorldSpaceSurroundingBounds( &vecMins, &vecMaxs );
CBaseTrace trace;
Ray_t ray;
float flFractionLeftSolid;
ray.Init( m_vecPrevPos, GetAbsOrigin(), WorldAlignMins(), WorldAlignMaxs() );
if ( IntersectRayWithBox( ray, vecMins, vecMaxs, 0.0, &trace, &flFractionLeftSolid ) )
{
// if bounding box check passes, check player hitboxes
trace_t trHitbox;
trace_t trWorld;
bool bTested = pOther->GetCollideable()->TestHitboxes( ray, MASK_SOLID | CONTENTS_HITBOX, trHitbox );
if ( !bTested || !trHitbox.DidHit() )
return;
// now, let's see if the flame visual could have actually hit this player. Trace backward from the
// point of impact to where the flame was fired, see if we hit anything. Since the point of impact was
// determined using the flame's bounding box and we're just doing a ray test here, we extend the
// start point out by the radius of the box.
Vector vDir = ray.m_Delta;
vDir.NormalizeInPlace();
UTIL_TraceLine( GetAbsOrigin() + vDir * WorldAlignMaxs().x, m_vecInitialPos, MASK_SOLID, this, COLLISION_GROUP_DEBRIS, &trWorld );
if ( tf_debug_flamethrower.GetInt() )
{
NDebugOverlay::Line( trWorld.startpos, trWorld.endpos, 0, 255, 0, true, 3.0f );
}
if ( trWorld.fraction == 1.0 )
{
// if there is nothing solid in the way, damage the entity
OnCollide( pOther );
}
else
{
// we hit the world, remove ourselves
*pbHitWorld = true;
UTIL_Remove( this );
}
}
}
//-----------------------------------------------------------------------------
// Purpose: Called when we've collided with another entity
//-----------------------------------------------------------------------------
void CTFFlameEntity::OnCollide( CBaseEntity *pOther )
{
// remember that we've burnt this player
m_hEntitiesBurnt.AddToTail( pOther );
float flDistance = GetAbsOrigin().DistTo( m_vecInitialPos );
float flMultiplier;
if ( flDistance <= 125 )
{
// at very short range, apply short range damage multiplier
flMultiplier = tf_flamethrower_shortrangedamagemultiplier.GetFloat();
}
else
{
// make damage ramp down from 100% to 25% from half the max dist to the max dist
flMultiplier = RemapValClamped( flDistance, tf_flamethrower_maxdamagedist.GetFloat()/2, tf_flamethrower_maxdamagedist.GetFloat(), 1.0, 0.25 );
}
float flDamage = m_flDmgAmount * flMultiplier;
flDamage = max( flDamage, 1.0 );
if ( tf_debug_flamethrower.GetInt() )
{
Msg( "Flame touch dmg: %.1f\n", flDamage );
}
CBaseEntity *pAttacker = m_hAttacker;
if ( !pAttacker )
return;
CTakeDamageInfo info( GetOwnerEntity(), pAttacker, flDamage, m_iDmgType, TF_DMG_CUSTOM_BURNING );
info.SetReportedPosition( pAttacker->GetAbsOrigin() );
// We collided with pOther, so try to find a place on their surface to show blood
trace_t pTrace;
UTIL_TraceLine( WorldSpaceCenter(), pOther->WorldSpaceCenter(), MASK_SOLID|CONTENTS_HITBOX, this, COLLISION_GROUP_NONE, &pTrace );
pOther->DispatchTraceAttack( info, GetAbsVelocity(), &pTrace );
ApplyMultiDamage();
}
#endif // GAME_DLL