hl2sdk/game/shared/tf/tf_flame.cpp
2025-02-19 18:36:16 -05:00

1210 lines
36 KiB
C++

//========= Copyright Valve Corporation, All rights reserved. ============//
//
//
//=============================================================================
#include "cbase.h"
#include "tf_flame.h"
#include "debugoverlay_shared.h"
#include "tf_gamerules.h"
#include "shot_manipulator.h"
#include "tf_weapon_flamethrower.h"
#ifdef GAME_DLL
#include "tf_player.h"
#include "tf_weapon_compound_bow.h"
#include "tf_logic_robot_destruction.h"
#include "ispatialpartition.h"
#include "tf_fx.h"
#endif // GAME_DLL
#ifdef CLIENT_DLL
#include "in_buttons.h"
#endif // CLIENT_DLL
const float tf_flame_burn_index_drain_rate = 1.25f;
const float tf_flame_burn_index_per_collide = 1.f;
const float tf_flame_burn_index_per_collide_remap_x = 10.f;
const float tf_flame_burn_index_per_collide_remap_y = 50.f;
const float tf_flame_burn_index_damage_scale_min = 0.5f;
ConVar tf_flame_dmg_mode_dist( "tf_flame_dmg_mode_dist", "0", FCVAR_REPLICATED | FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY | FCVAR_HIDDEN );
#ifdef WATERFALL_FLAMETHROWER_TEST
ConVar tf_flame_waterfall_speed_override( "tf_flame_waterfall_speed_override", "0", FCVAR_REPLICATED );
ConVar tf_flame_waterfall_drag_override( "tf_flame_waterfall_drag_override", "0", FCVAR_REPLICATED );
ConVar tf_flame_waterfall_gravity_override( "tf_flame_waterfall_gravity_override", "0", FCVAR_REPLICATED );
ConVar tf_flame_waterfall_lifetime_override( "tf_flame_waterfall_lifetime_override", "0", FCVAR_REPLICATED,
"Time to live of flame damage entities." );
ConVar tf_flame_waterfall_spread( "tf_flame_waterfall_spread", "40", FCVAR_REPLICATED,
"Spread angle of flame in waterfall mode." );
#endif // WATERFALL_FLAMETHROWER_TEST
extern ConVar tf_debug_flamethrower;
extern ConVar tf_flamethrower_boxsize;
#ifdef CLIENT_DLL
float tf_flame_particle_min_density = 0.01f;
#else // CLIENT_DLL
const float tf_flame_min_damage_scale = 0.5f;
const float tf_flame_maxdamagedist = 150.f;
const float tf_flame_mindamagedist = 300.f;
const float tf_flame_min_damage_scale_time = 0.5f;
const float tf_flame_min_damage_scale_time_cap = 0.5f;
#endif
IMPLEMENT_NETWORKCLASS_ALIASED( TFFlameManager, DT_TFFlameManager );
BEGIN_NETWORK_TABLE( CTFFlameManager, DT_TFFlameManager )
#ifdef GAME_DLL
SendPropEHandle( SENDINFO( m_hWeapon ) ),
SendPropEHandle( SENDINFO( m_hAttacker ) ),
SendPropFloat( SENDINFO( m_flSpreadDegree ) ),
SendPropFloat( SENDINFO( m_flRedirectedFlameSizeMult ) ),
SendPropFloat( SENDINFO( m_flFlameStartSizeMult ) ),
SendPropFloat( SENDINFO( m_flFlameEndSizeMult ) ),
SendPropFloat( SENDINFO( m_flFlameIgnorePlayerVelocity ) ),
SendPropFloat( SENDINFO( m_flFlameReflectionAdditionalLifeTime ) ),
SendPropFloat( SENDINFO( m_flFlameReflectionDamageReduction ) ),
SendPropInt( SENDINFO( m_iMaxFlameReflectionCount ) ),
SendPropInt( SENDINFO( m_nShouldReflect ) ),
SendPropFloat( SENDINFO( m_flFlameSpeed ) ),
SendPropFloat( SENDINFO( m_flFlameLifeTime ) ),
SendPropFloat( SENDINFO( m_flRandomLifeTimeOffset ) ),
SendPropFloat( SENDINFO( m_flFlameGravity ) ),
SendPropFloat( SENDINFO( m_flFlameDrag ) ),
SendPropFloat( SENDINFO( m_flFlameUp ) ),
SendPropBool( SENDINFO( m_bIsFiring ) ),
#ifdef WATERFALL_FLAMETHROWER_TEST
SendPropInt( SENDINFO( m_iWaterfallMode ) ),
SendPropArray( SendPropEHandle( SENDINFO_ARRAY( m_hAdditionalFlameManagers ) ), m_hAdditionalFlameManagers ),
SendPropInt( SENDINFO( m_iStreamIndex ) ),
#endif // WATERFALL_FLAMETHROWER_TEST
#else
RecvPropEHandle( RECVINFO( m_hWeapon ) ),
RecvPropEHandle( RECVINFO( m_hAttacker ) ),
RecvPropFloat( RECVINFO( m_flSpreadDegree ) ),
RecvPropFloat( RECVINFO( m_flRedirectedFlameSizeMult ) ),
RecvPropFloat( RECVINFO( m_flFlameStartSizeMult ) ),
RecvPropFloat( RECVINFO( m_flFlameEndSizeMult ) ),
RecvPropFloat( RECVINFO( m_flFlameIgnorePlayerVelocity ) ),
RecvPropFloat( RECVINFO( m_flFlameReflectionAdditionalLifeTime ) ),
RecvPropFloat( RECVINFO( m_flFlameReflectionDamageReduction ) ),
RecvPropInt( RECVINFO( m_iMaxFlameReflectionCount ) ),
RecvPropInt( RECVINFO( m_nShouldReflect ) ),
RecvPropFloat( RECVINFO( m_flFlameSpeed ) ),
RecvPropFloat( RECVINFO( m_flFlameLifeTime ) ),
RecvPropFloat( RECVINFO( m_flRandomLifeTimeOffset ) ),
RecvPropFloat( RECVINFO( m_flFlameGravity ) ),
RecvPropFloat( RECVINFO( m_flFlameDrag ) ),
RecvPropFloat( RECVINFO( m_flFlameUp ) ),
RecvPropBool( RECVINFO( m_bIsFiring ) ),
#ifdef WATERFALL_FLAMETHROWER_TEST
RecvPropInt( RECVINFO( m_iWaterfallMode ) ),
RecvPropArray( RecvPropEHandle( RECVINFO( m_hAdditionalFlameManagers[0] ) ), m_hAdditionalFlameManagers ),
RecvPropInt( RECVINFO( m_iStreamIndex ) ),
#endif // WATERFALL_FLAMETHROWER_TEST
#endif
END_NETWORK_TABLE()
BEGIN_DATADESC( CTFFlameManager )
END_DATADESC()
#if defined( CLIENT_DLL )
BEGIN_PREDICTION_DATA( CTFFlameManager )
DEFINE_PRED_FIELD( m_bIsFiring, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE ),
END_PREDICTION_DATA()
#endif
LINK_ENTITY_TO_CLASS( tf_flame_manager, CTFFlameManager );
IMPLEMENT_AUTO_LIST( ITFFlameManager );
CTFFlameManager::CTFFlameManager()
#ifdef GAME_DLL
: m_mapEntitiesBurnt( DefLessFunc(EHANDLE) )
#endif // GAME_DLL
{
// default attr
m_flSpreadDegree = 0.f;
m_flRedirectedFlameSizeMult = 1.f;
m_flFlameStartSizeMult = 1.f;
m_flFlameEndSizeMult = 1.f;
m_flFlameIgnorePlayerVelocity = 0.f;
m_flFlameReflectionAdditionalLifeTime = 0.f;
m_flFlameReflectionDamageReduction = 1.f;
m_iMaxFlameReflectionCount = 0;
m_nShouldReflect = 0;
m_flFlameSpeed = 0.f;
m_flFlameLifeTime = 0.f;
m_flRandomLifeTimeOffset = 0.f;
m_flFlameGravity = 0.f;
m_flFlameDrag = 0.f;
m_flFlameUp = 0.f;
m_bIsFiring = false;
#ifdef WATERFALL_FLAMETHROWER_TEST
m_iWaterfallMode = 0;
m_iStreamIndex = -1;
#endif // WATERFALL_FLAMETHROWER_TEST
#ifdef CLIENT_DLL
m_nMuzzleAttachment = INVALID_PARTICLE_ATTACHMENT;
#endif // CLIENT_DLL
}
void CTFFlameManager::UpdateOnRemove( void )
{
#ifdef CLIENT_DLL
RemoveAllParticles();
#endif // CLIENT_DLL
#ifdef GAME_DLL
#ifdef WATERFALL_FLAMETHROWER_TEST
// This is temp -- if we want to ship this the multiple streams should be at flame manager level instead of
// creating many entities/particle-systems/etc
for ( int idx = 0; idx < m_hAdditionalFlameManagers.Count(); idx++ )
{
auto &hFM = m_hAdditionalFlameManagers[idx];
if ( hFM )
{
UTIL_Remove( hFM );
m_hAdditionalFlameManagers.Set( idx, NULL );
}
}
#endif // WATERFALL_FLAMETHROWER_TEST
#endif // GAME_DLL
BaseClass::UpdateOnRemove();
}
void CTFFlameManager::StartFiring()
{
if ( m_bIsFiring )
return;
m_bIsFiring = true;
#ifdef CLIENT_DLL
OnFiringStateChange();
#endif // CLIENT_DLL
}
void CTFFlameManager::StopFiring()
{
if ( !m_bIsFiring )
return;
m_bIsFiring = false;
#ifdef CLIENT_DLL
OnFiringStateChange();
#endif // CLIENT_DLL
}
#ifdef CLIENT_DLL
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameManager::OnPreDataChanged( DataUpdateType_t updateType )
{
BaseClass::OnPreDataChanged( updateType );
m_bOldFiring = m_bIsFiring;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameManager::OnDataChanged( DataUpdateType_t updateType )
{
BaseClass::OnDataChanged( updateType );
if ( m_bOldFiring != m_bIsFiring )
{
OnFiringStateChange();
}
}
#endif // CLIENT_DLL
void DebugFlame( const Vector& vStart, const Vector& vEnd, const Vector& vMin, const Vector& vMax, const Color& color )
{
NDebugOverlay::Line( vStart, vEnd, 255, 255, 0, false, 0.0f );
NDebugOverlay::SweptBox( vStart, vEnd, vMin, vMax, vec3_angle, color.r(), color.g(), color.b(), color.a(), 0.0f );
}
void CTFFlameManager::InitializePoint( tf_point_t *pPoint, int nPointIndex )
{
BaseClass::InitializePoint( pPoint, nPointIndex );
flame_point_t *pFlame = static_cast< flame_point_t * >( pPoint );
if ( m_hAttacker )
{
pFlame->m_vecAttackerVelocity = m_hAttacker->GetLocalVelocity();
pFlame->m_vecInitialPos = pPoint->m_vecPosition;
}
}
Vector CTFFlameManager::GetInitialPosition() const
{
if ( GetOwnerEntity() )
{
CTFFlameThrower *pFlameThrower = assert_cast< CTFFlameThrower* >( GetOwnerEntity() );
if ( pFlameThrower )
{
return pFlameThrower->GetFlameOriginPos();
}
}
return BaseClass::GetInitialPosition();
}
Vector CTFFlameManager::GetInitialVelocity() const
{
if ( m_hAttacker && m_hAttacker->IsPlayer() )
{
CTFPlayer *pTFPlayer = ToTFPlayer( m_hAttacker );
if ( pTFPlayer )
{
QAngle angFlame = pTFPlayer->EyeAngles();
#ifdef WATERFALL_FLAMETHROWER_TEST
if ( m_iWaterfallMode && m_iStreamIndex >= 0 )
{
const int nAdditionalStreams = m_hAdditionalFlameManagers.Count();
Assert( nAdditionalStreams == WATERFALL_FLAMETHROWER_STREAMS - 1 );
Assert( nAdditionalStreams % 2 == 0 ); // Assuming an odd number of total streams
// Total spread
int nDegSpread = tf_flame_waterfall_spread.GetInt();
// Variance per flame to fill out the fan effect
int nPerFlameSpread = nDegSpread / WATERFALL_FLAMETHROWER_STREAMS;
// The central stream is always firing forward, so insert a phantom offset halfway through the
// range, then remap the range to the spread.
int streamNum = m_iStreamIndex + ( m_iStreamIndex >= nAdditionalStreams / 2 );
int degRandOffset = m_randomStream.RandomInt( -nPerFlameSpread / 2, nPerFlameSpread / 2 );
int degStream = RemapValClamped( streamNum, 0, nAdditionalStreams, -nDegSpread / 2, nDegSpread / 2);
angFlame[YAW] += degStream + degRandOffset;
}
#endif // WATERFALL_FLAMETHROWER_TEST
Vector vDirection;
AngleVectors( angFlame, &vDirection );
// apply spread to velocity if needed
if ( m_flSpreadDegree > 0.f )
{
CShotManipulator shotManipulator( vDirection );
float flSpreadDeg2Rad = DEG2RAD( m_flSpreadDegree );
vDirection = shotManipulator.ApplySpread( Vector( flSpreadDeg2Rad, flSpreadDeg2Rad, flSpreadDeg2Rad ), 1.f, &m_randomStream );
}
return GetInitialSpeed() * vDirection;
}
}
return BaseClass::GetInitialVelocity();
}
#ifdef GAME_DLL
class CFlameManagerHelper : public CAutoGameSystemPerFrame
{
public:
CFlameManagerHelper( const char *pszName ) : CAutoGameSystemPerFrame( pszName )
{
}
// called after entities think
virtual void FrameUpdatePostEntityThink() OVERRIDE
{
FOR_EACH_VEC( ITFFlameManager::AutoList(), i )
{
CTFFlameManager *pFlameManager = static_cast< CTFFlameManager* >( ITFFlameManager::AutoList()[i] );
pFlameManager->PostEntityThink();
}
}
};
static CFlameManagerHelper s_flameManagerHelper( "flame_manager_helper" );
void CTFFlameManager::PostEntityThink( void )
{
// remove all entities that are no longer touching the flame
FOR_EACH_MAP_FAST( m_mapEntitiesBurnt, i )
{
if ( gpGlobals->curtime - m_mapEntitiesBurnt[i].m_flLastBurnTime > m_flBurnFrequency )
{
m_mapEntitiesBurnt.RemoveAt( i );
// go thru the map again to see if we need to remove anything else
i = 0;
}
}
FOR_EACH_MAP_FAST( m_mapEntitiesBurnt, i )
{
// Decay heat index (colliding refreshes it)
// TODO(driller): factor in time since last think
m_mapEntitiesBurnt[i].m_flHeatIndex = Max( m_mapEntitiesBurnt[i].m_flHeatIndex - tf_flame_burn_index_drain_rate, 0.f );
}
CTFFlameThrower *pFlameThrower = NULL;
if ( m_hWeapon.Get() && m_hWeapon.Get()->GetWeaponID() == TF_WEAPON_FLAMETHROWER )
{
pFlameThrower = assert_cast< CTFFlameThrower* >( m_hWeapon.Get() );
}
if ( pFlameThrower )
{
pFlameThrower->ResetFlameHitCount();
if ( m_mapEntitiesBurnt.Count() > 0 )
{
// this is to keep focus burn sound the same as the old flame
pFlameThrower->IncrementFlameDamageCount();
pFlameThrower->IncrementActiveFlameCount();
}
}
}
CTFFlameManager* CTFFlameManager::Create( CBaseEntity *pOwner, bool bIsAdditionalManager /*= false*/ )
{
CTFFlameManager *pFlameManager = static_cast<CTFFlameManager*>( CBaseEntity::Create( "tf_flame_manager", vec3_origin, vec3_angle, pOwner ) );
if ( pFlameManager )
{
// Initialize the owner.
pFlameManager->SetOwnerEntity( pOwner );
if ( pOwner->GetOwnerEntity() )
pFlameManager->m_hAttacker = pOwner->GetOwnerEntity();
else
pFlameManager->m_hAttacker = pOwner;
// Track total active flame entities
pFlameManager->m_hWeapon = dynamic_cast< CTFWeaponBase* >( pOwner );
// Set team.
pFlameManager->ChangeTeam( pOwner->GetTeamNumber() );
pFlameManager->HookAttributes();
#ifdef WATERFALL_FLAMETHROWER_TEST
if ( !bIsAdditionalManager )
{
// Temp prototype hack, going to re-do properly if we want to keep this
//
// This is temp -- if we want to ship this the multiple streams should be at flame manager level instead of
// creating many entities/particle-systems/etc
if ( pFlameManager->m_iWaterfallMode )
{
for ( int idx = 0; idx < pFlameManager->m_hAdditionalFlameManagers.Count(); idx++ )
{
auto &hFM = pFlameManager->m_hAdditionalFlameManagers[idx];
if ( !hFM )
{
CTFFlameManager *pAdditionalFlameManager = CTFFlameManager::Create( pOwner, true );
if ( pAdditionalFlameManager )
{
pFlameManager->m_hAdditionalFlameManagers.Set( idx, pAdditionalFlameManager );
pAdditionalFlameManager->m_iStreamIndex = idx;
}
}
}
}
}
#endif // WATERFALL_FLAMETHROWER_TEST
}
return pFlameManager;
}
bool CTFFlameManager::AddPoint( int iCurrentTick )
{
bool bRet = BaseClass::AddPoint( iCurrentTick );
#ifdef WATERFALL_FLAMETHROWER_TEST
// Temp prototype hack, going to re-do properly if we want to keep this
//
// This is temp -- if we want to ship this the multiple streams should be at flame manager level instead of
// creating many entities/particle-systems/etc
if ( m_iWaterfallMode )
{
for ( int idx = 0; idx < m_hAdditionalFlameManagers.Count(); idx++ )
{
auto &hFM = m_hAdditionalFlameManagers[idx];
if ( hFM )
{
bRet &= hFM->AddPoint( iCurrentTick );
}
}
}
#endif // WATERFALL_FLAMETHROWER_TEST
return bRet;
}
void CTFFlameManager::HookAttributes()
{
// cache all attrs
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flSpreadDegree, flame_spread_degree );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flRedirectedFlameSizeMult, redirected_flame_size_mult );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flFlameStartSizeMult, mult_flame_size );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flFlameEndSizeMult, mult_end_flame_size );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flFlameIgnorePlayerVelocity, flame_ignore_player_velocity );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flFlameReflectionAdditionalLifeTime, flame_reflection_add_life_time );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flFlameReflectionDamageReduction, reflected_flame_dmg_reduction );
CALL_ATTRIB_HOOK_INT_ON_OTHER( m_hAttacker, m_iMaxFlameReflectionCount, max_flame_reflection_count );
CALL_ATTRIB_HOOK_INT_ON_OTHER( m_hAttacker, m_nShouldReflect, flame_reflect_on_collision );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flFlameSpeed, flame_speed );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flFlameLifeTime, flame_lifetime );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flRandomLifeTimeOffset, flame_random_life_time_offset );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flFlameGravity, flame_gravity );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flFlameDrag, flame_drag );
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_flFlameUp, flame_up_speed );
#ifdef WATERFALL_FLAMETHROWER_TEST
CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( m_hAttacker, m_iWaterfallMode, flame_waterfall );
#endif // WATERFALL_FLAMETHROWER_TEST
}
void CTFFlameManager::UpdateDamage( int iDmgType, float flDamage, float flBurnFrequency, bool bCritFromBehind )
{
m_iDmgType = iDmgType;
m_flDamage = flDamage;
m_flBurnFrequency = flBurnFrequency;
m_bCritFromBehind = bCritFromBehind;
#ifdef WATERFALL_FLAMETHROWER_TEST
if ( m_iWaterfallMode )
{
for ( int idx = 0; idx < m_hAdditionalFlameManagers.Count(); idx++ )
{
auto &hFM = m_hAdditionalFlameManagers[idx];
if ( hFM )
{
hFM->UpdateDamage( iDmgType, flDamage, flBurnFrequency, bCritFromBehind );
}
}
}
#endif // WATERFALL_FLAMETHROWER_TEST
}
float CTFFlameManager::GetFlameDamageScale( const tf_point_t* pPoint, CTFPlayer *pTFTarget /*= NULL*/ ) const
{
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
const flame_point_t *pFlame = static_cast< const flame_point_t * >( pPoint );
float flDamageScale = 1.f;
// Distance-based calculation is what we shipped with
if ( tf_flame_dmg_mode_dist.GetBool() )
{
float flDistSqr = pFlame->m_vecPosition.DistToSqr( pFlame->m_vecInitialPos );
float flMaxDamageDistSqr = Square( tf_flame_maxdamagedist );
float flMinDamageDistSqr = Square( tf_flame_mindamagedist );
flDamageScale = RemapValClamped( flDistSqr, flMaxDamageDistSqr, flMinDamageDistSqr, 1.0f, tf_flame_min_damage_scale );
}
// Lifetime-based
else
{
float flTimeAlive = gpGlobals->curtime - pFlame->m_flSpawnTime;
float flLifeMax = pFlame->m_flLifeTime * tf_flame_min_damage_scale_time_cap;
flDamageScale = RemapValClamped( flTimeAlive, 0.f, flLifeMax, 1.f, tf_flame_min_damage_scale_time );
}
if ( pTFTarget
)
{
float flIndexMod = 1.f;
auto iEntIndex = m_mapEntitiesBurnt.Find( pTFTarget );
if ( iEntIndex != m_mapEntitiesBurnt.InvalidIndex() )
{
flIndexMod = RemapValClamped( m_mapEntitiesBurnt[iEntIndex].m_flHeatIndex,
tf_flame_burn_index_per_collide_remap_x, tf_flame_burn_index_per_collide_remap_y,
tf_flame_burn_index_damage_scale_min, 1.f );
}
flDamageScale *= flIndexMod;
}
// should we reduce damage based on reflection?
for ( int i = 0; i<pPoint->m_nHitWall; ++i )
{
flDamageScale *= ReflectionDamageReduction();
}
return flDamageScale;
}
bool CTFFlameManager::BCanBurnEntityThisFrame( CBaseEntity *pEnt ) const
{
auto iBurnt = m_mapEntitiesBurnt.Find( pEnt );
if ( iBurnt == m_mapEntitiesBurnt.InvalidIndex() )
return true;
float flLastBurnTime = m_mapEntitiesBurnt[iBurnt].m_flLastBurnTime;
return ( gpGlobals->curtime - flLastBurnTime >= m_flBurnFrequency );
}
void CTFFlameManager::SetHitTarget( void )
{
if ( !m_hWeapon.Get() )
return;
if ( m_hWeapon.Get()->GetWeaponID() == TF_WEAPON_FLAMETHROWER )
{
assert_cast< CTFFlameThrower* >( m_hWeapon.Get() )->SetHitTarget();
}
}
bool CFlameEntityEnum::EnumEntity( IHandleEntity *pHandleEntity )
{
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
CBaseEntity *pEnt = static_cast< CBaseEntity* >( pHandleEntity );
// Ignore collisions with the shooter
if ( pEnt == m_pShooter )
return true;
if ( pEnt->IsPlayer() && pEnt->IsAlive() )
{
m_Targets.AddToTail( pEnt );
}
else if ( pEnt->MyNextBotPointer() && pEnt->IsAlive() )
{
// add non-player bots
m_Targets.AddToTail( pEnt );
}
else if ( pEnt->IsBaseObject() && m_pShooter->GetTeamNumber() != pEnt->GetTeamNumber() )
{
// only add enemy objects
m_Targets.AddToTail( pEnt );
}
else if ( CTFRobotDestructionLogic::GetRobotDestructionLogic() && m_pShooter->GetTeamNumber() != pEnt->GetTeamNumber() && FClassnameIs( pEnt, "tf_robot_destruction_robot" ) )
{
// only add enemy robots
m_Targets.AddToTail( pEnt );
}
else if ( FClassnameIs( pEnt, "func_breakable" ) || FClassnameIs( pEnt, "tf_pumpkin_bomb" ) || FClassnameIs( pEnt, "tf_merasmus_trick_or_treat_prop" ) || FClassnameIs( pEnt, "tf_generic_bomb" ) )
{
m_Targets.AddToTail( pEnt );
}
return true;
}
bool CTFFlameManager::IsValidBurnTarget( CBaseEntity *pEntity ) const
{
// is our attacker still valid?
if ( !m_hAttacker.Get() )
return false;
// Ignore collisions with the shooter
if ( pEntity == m_hAttacker )
return false;
if ( pEntity->IsPlayer() && pEntity->IsAlive() )
{
return true;
}
else if ( pEntity->MyNextBotPointer() && pEntity->IsAlive() )
{
// add non-player bots
return true;
}
else if ( pEntity->IsBaseObject() && m_hAttacker->GetTeamNumber() != pEntity->GetTeamNumber() )
{
// only add enemy objects
return true;
}
else if ( CTFRobotDestructionLogic::GetRobotDestructionLogic() && m_hAttacker->GetTeamNumber() != pEntity->GetTeamNumber() && FClassnameIs( pEntity, "tf_robot_destruction_robot" ) )
{
// only add enemy robots
return true;
}
else if ( FClassnameIs( pEntity, "func_breakable" ) || FClassnameIs( pEntity, "tf_pumpkin_bomb" ) || FClassnameIs( pEntity, "tf_merasmus_trick_or_treat_prop" ) || FClassnameIs( pEntity, "tf_generic_bomb" ) )
{
return true;
}
return false;
}
bool CTFFlameManager::ShouldCollide( CBaseEntity *pEnt ) const
{
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
if ( !IsValidBurnTarget( pEnt ) )
return false;
#ifdef WATERFALL_FLAMETHROWER_TEST
if ( m_iWaterfallMode )
{
for ( int idx = 0; idx < m_hAdditionalFlameManagers.Count(); idx++ )
{
auto &hFM = m_hAdditionalFlameManagers[idx];
if ( hFM && !hFM->BCanBurnEntityThisFrame( pEnt ) )
{
return false;
}
}
}
#endif // WATERFALL_FLAMETHROWER_TEST
return true;
}
void CTFFlameManager::OnCollide( CBaseEntity *pEnt, int iPointIndex )
{
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
CBaseEntity *pAttacker = m_hAttacker;
if ( !pAttacker )
return;
const flame_point_t *pFlame = static_cast<const flame_point_t *>( GetPointVec()[iPointIndex] );
// Do this each touch - even if we're not doing damage - to generate a heat index (rough approximation of density/accuracy - per-target)
int iEntIndex = m_mapEntitiesBurnt.Find( pEnt );
if ( iEntIndex != m_mapEntitiesBurnt.InvalidIndex() )
{
float flTimeAlive = gpGlobals->curtime - pFlame->m_flSpawnTime;
float flAmount = RemapValClamped( flTimeAlive, 0.f, 0.02f, ( tf_flame_burn_index_per_collide * 2.f ), tf_flame_burn_index_per_collide );
m_mapEntitiesBurnt[iEntIndex].m_flHeatIndex += flAmount;
}
// if we already burn this entity, check if we can burn it again
if ( !BCanBurnEntityThisFrame( pEnt ) )
return;
if ( pEnt->IsPlayer() && pEnt->InSameTeam( pAttacker ) )
{
CTFPlayer *pPlayer = ToTFPlayer( pEnt );
// Only care about Snipers
if ( !pPlayer->IsPlayerClass(TF_CLASS_SNIPER) )
return;
// Does he have the bow?
CTFWeaponBase *pWpn = pPlayer->GetActiveTFWeapon();
if ( pWpn && pWpn->GetWeaponID() == TF_WEAPON_COMPOUND_BOW )
{
CTFCompoundBow *pBow = static_cast<CTFCompoundBow*>( pWpn );
pBow->SetArrowAlight( true );
}
}
else
{
SetHitTarget();
int iDamageType = m_iDmgType;
CTFPlayer *pVictim = NULL;
if ( pEnt->IsPlayer() )
{
pVictim = ToTFPlayer( pEnt );
// get direction of touching flame from initial pos to current pos
Vector vFlameGeneralDir = ( pFlame->m_vecPosition - pFlame->m_vecInitialPos ).Normalized();
Vector vOtherForward;
AngleVectors( pEnt->GetAbsAngles(), &vOtherForward );
vOtherForward.z = 0;
vOtherForward.NormalizeInPlace();
const float flBehindThreshold = 0.8f;
// check if we're behind the victim
if ( DotProduct( vFlameGeneralDir, vOtherForward ) > flBehindThreshold )
{
if ( m_bCritFromBehind == true )
{
iDamageType |= DMG_CRITICAL;
}
if ( pVictim )
{
pVictim->HandleAchievement_Pyro_BurnFromBehind( ToTFPlayer( pAttacker ) );
}
}
// Pyro-specific
if ( pAttacker->IsPlayer() && pVictim )
{
CTFPlayer *pPlayerAttacker = ToTFPlayer( pAttacker );
if ( pPlayerAttacker && pPlayerAttacker->IsPlayerClass( TF_CLASS_PYRO ) )
{
// burn the victim while taunting?
if ( pVictim->m_Shared.InCond( TF_COND_TAUNTING ) )
{
static CSchemaItemDefHandle flipTaunt( "Flippin' Awesome Taunt" );
// if I'm the one being flipped, and getting lit on fire
if ( !pVictim->IsTauntInitiator() && pVictim->GetTauntEconItemView() && pVictim->GetTauntEconItemView()->GetItemDefinition() == flipTaunt )
{
pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_PYRO_IGNITE_PLAYER_BEING_FLIPPED );
}
}
pVictim->m_Shared.AddCond( TF_COND_HEALING_DEBUFF, 2.f, pAttacker );
}
}
}
// make sure damage is at least 1
float flDamageScale = GetFlameDamageScale( pFlame, pVictim );
float flDamage = MAX( flDamageScale * m_flDamage, 1.f );
CTakeDamageInfo info( GetOwnerEntity(), pAttacker, GetOwnerEntity(), flDamage, iDamageType, TF_DMG_CUSTOM_BURNING );
info.SetReportedPosition( pAttacker->GetAbsOrigin() );
if ( info.GetDamageType() & DMG_CRITICAL )
{
info.SetCritType( CTakeDamageInfo::CRIT_FULL );
}
// terrible hack for flames hitting the Merasmus props to get the particle effect in the correct position
if ( TFGameRules() && TFGameRules()->GetActiveBoss() && ( TFGameRules()->GetActiveBoss()->GetBossType() == HALLOWEEN_BOSS_MERASMUS ) )
{
info.SetDamagePosition( GetAbsOrigin() );
}
// Track hits for the Flamethrower, which is used to change the weapon sound based on hit ratio
/*if ( m_hFlameThrower )
{
m_bBurnedEnemy = true;
m_hFlameThrower->IncrementFlameDamageCount();
}*/
// We collided with pEnt, so try to find a place on their surface to show blood
trace_t pTrace;
UTIL_TraceLine( WorldSpaceCenter(), pEnt->WorldSpaceCenter(), MASK_SOLID|CONTENTS_HITBOX, this, COLLISION_GROUP_NONE, &pTrace );
pEnt->DispatchTraceAttack( info, GetAbsVelocity(), &pTrace );
ApplyMultiDamage();
}
// add ent to burn list
if ( iEntIndex != m_mapEntitiesBurnt.InvalidIndex() )
{
m_mapEntitiesBurnt[ iEntIndex ].m_flLastBurnTime = gpGlobals->curtime;
}
else
{
m_mapEntitiesBurnt.Insert( pEnt, { gpGlobals->curtime, tf_flame_burn_index_per_collide } );
}
}
#endif // GAME_DLL
float CTFFlameManager::GetFlameSizeMult( const tf_point_t *pPoint ) const
{
float flLife = gpGlobals->curtime - pPoint->m_flSpawnTime;
float flSizeMult = RemapValClamped( flLife, 0.f, pPoint->m_flLifeTime, GetStartSizeMult(), GetEndSizeMult() );
if ( pPoint->m_nHitWall > 0 )
{
flSizeMult *= m_flRedirectedFlameSizeMult;
}
return flSizeMult;
}
float CTFFlameManager::GetStartSizeMult() const
{
return m_flFlameStartSizeMult;
}
float CTFFlameManager::GetEndSizeMult() const
{
return m_flFlameEndSizeMult;
}
bool CTFFlameManager::ShouldIgnorePlayerVelocity() const
{
return m_flFlameIgnorePlayerVelocity != 0.f;
}
float CTFFlameManager::ReflectionAdditionalLifeTime() const
{
return m_flFlameReflectionAdditionalLifeTime;
}
float CTFFlameManager::ReflectionDamageReduction() const
{
return m_flFlameReflectionDamageReduction;
}
int CTFFlameManager::GetMaxFlameReflectionCount() const
{
return m_iMaxFlameReflectionCount;
}
#ifdef CLIENT_DLL
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameManager::OnClientPointAdded( const tf_point_t *pNewestPoint )
{
//DevMsg( "OnClientPointAdded\n" );
// ignore new points if we don't have particle effect
if ( !m_hParticleEffect )
{
return;
}
const flame_point_t* pNewestFlame = static_cast< const flame_point_t * >( pNewestPoint );
if ( pNewestFlame )
{
UpdateWeaponParticleControlPoint( pNewestFlame );
UpdateFlameParticleControlPoint( pNewestFlame );
// set active CP to the newest point
int nTargetCP = pNewestFlame->m_nPointIndex + 1;
Assert( nTargetCP != 0 );
m_hParticleEffect->SetControlPointIndex( nTargetCP );
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameManager::UpdateWeaponParticleControlPoint( const flame_point_t *pNewestFlame )
{
if ( !m_hParticleEffect )
return;
if ( !m_hWeapon )
return;
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s: Update particle", __FUNCTION__ );
const float flStartRadius = 3.f;
Vector vMuzzlePos;
QAngle qMuzzleAng;
if ( m_nMuzzleAttachment == INVALID_PARTICLE_ATTACHMENT )
{
m_nMuzzleAttachment = m_hWeapon->LookupAttachment( "muzzle" );
}
m_hWeapon->GetAttachment( m_nMuzzleAttachment, vMuzzlePos, qMuzzleAng );
if ( m_hParticleEffect )
{
Vector vMuzzleForward, vMuzzleRight, vMuzzleUp;
AngleVectors( qMuzzleAng, &vMuzzleForward, &vMuzzleRight, &vMuzzleUp );
m_hParticleEffect->SetControlPoint( 0, vMuzzlePos );
m_hParticleEffect->SetControlPointOrientation( 0, vMuzzleForward, vMuzzleRight, vMuzzleUp );
m_hParticleEffect->SetControlPointDensity( 0, 1.f );
m_hParticleEffect->SetControlPointRadius( 0, flStartRadius );
Vector vVelocity = pNewestFlame ? pNewestFlame->m_vecAttackerVelocity + pNewestFlame->m_vecVelocity : vec3_origin;
m_hParticleEffect->SetControlPointVelocity( 0, vVelocity );
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameManager::UpdateFlameParticleControlPoint( const flame_point_t *pFlame )
{
Assert( pFlame );
float flRadius = GetRadius( pFlame );
float flAge = gpGlobals->curtime - pFlame->m_flSpawnTime;
if ( m_hParticleEffect )
{
int iControlPoint = pFlame->m_nPointIndex + 1;
Assert( iControlPoint != 0 );
m_hParticleEffect->SetControlPoint( iControlPoint, pFlame->m_vecPosition );
float flDensity = RemapValClamped( flAge, 0.f, pFlame->m_flLifeTime, 1.f, tf_flame_particle_min_density );
m_hParticleEffect->SetControlPointDensity( iControlPoint, flDensity );
m_hParticleEffect->SetControlPointRadius( iControlPoint, flRadius );
m_hParticleEffect->SetControlPointDuration( iControlPoint, pFlame->m_flLifeTime );
Vector vecVelocity = pFlame->m_vecAttackerVelocity + pFlame->m_vecVelocity;
m_hParticleEffect->SetControlPointVelocity( iControlPoint, vecVelocity );
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameManager::OnFiringStateChange()
{
// stop emission for old particle if needed particles
if ( m_hParticleEffect && !m_hParticleEffect->m_bEmissionStopped )
{
//DevMsg( "stop old particle\n" );
ParticleProp()->StopEmission( m_hParticleEffect );
m_hOldParticleEffects.AddToTail( m_hParticleEffect );
}
if ( m_bIsFiring )
{
//DevMsg( "start new particle\n" );
CTFFlameThrower *pFlameThrower = NULL;
if ( m_hWeapon )
{
pFlameThrower = dynamic_cast< CTFFlameThrower* >( m_hWeapon.Get() );
}
const char *pszParticleName = pFlameThrower ? pFlameThrower->GetParticleEffectName() : "new_flame";
m_hParticleEffect = ParticleProp()->Create( pszParticleName, PATTACH_CUSTOMORIGIN, 0 );
if ( !m_hParticleEffect )
{
Warning( "Failed to create %s particles.", pszParticleName );
}
else
{
UpdateWeaponParticleControlPoint( NULL );
}
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFFlameManager::RemoveAllParticles()
{
if ( m_hParticleEffect )
{
ParticleProp()->StopEmissionAndDestroyImmediately( m_hParticleEffect );
m_hParticleEffect = NULL;
}
ParticleProp()->StopEmission( NULL, false, true );
}
#endif // CLIENT_DLL
void CTFFlameManager::Update()
{
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
BaseClass::Update();
if ( tf_debug_flamethrower.GetBool() )
{
#ifdef GAME_DLL
Color color( 0, 0, 255, 100 );
#else
Color color( 255, 0, 0, 100 );
#endif
FOR_EACH_VEC( GetPointVec(), i )
{
const flame_point_t *pFlame = static_cast< const flame_point_t * >( GetPointVec()[i] );
float flRadius = GetRadius( pFlame );
Vector vMins = flRadius * Vector( -1, -1, -1 );
Vector vMaxs = flRadius * Vector( 1, 1, 1 );
DebugFlame( pFlame->m_vecPrevPosition, pFlame->m_vecPosition, vMins, vMaxs, color );
}
}
#ifdef GAME_DLL
#else // GAME_DLL
if ( GetPointVec().IsEmpty() )
{
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s: Empty stop particle immediate", __FUNCTION__ );
if ( !m_bIsFiring )
{
RemoveAllParticles();
}
return;
}
// remove old particles if they're no longer active
FOR_EACH_VEC_BACK( m_hOldParticleEffects, i )
{
if ( m_hOldParticleEffects[i]->m_nActiveParticles == 0 )
{
ParticleProp()->StopEmission( m_hOldParticleEffects[i] );
m_hOldParticleEffects[i] = NULL;
m_hOldParticleEffects.Remove( i );
}
}
const flame_point_t* pNewestFlame = static_cast< const flame_point_t * >( GetPointVec()[ GetPointVec().Count() - 1 ] );
UpdateWeaponParticleControlPoint( pNewestFlame );
FOR_EACH_VEC( GetPointVec(), i )
{
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s: FOR_EACH_VEC( GetPointVec() ) ", __FUNCTION__ );
const flame_point_t *pFlame = static_cast< const flame_point_t * >( GetPointVec()[i] );
UpdateFlameParticleControlPoint( pFlame );
}
#endif // CLIENT_DLL
}
bool CTFFlameManager::OnPointHitWall( tf_point_t *pPoint, Vector &vecNewPos, Vector &vecNewVelocity, const trace_t& tr, float flDT )
{
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
// bounce too many tiems
if ( GetMaxFlameReflectionCount() > 0 && pPoint->m_nHitWall > GetMaxFlameReflectionCount() )
{
return true;
}
flame_point_t *pFlame = static_cast< flame_point_t* >( pPoint );
pFlame->m_flLifeTime += ReflectionAdditionalLifeTime();
Vector vecNewDirVelocity = vecNewVelocity - DotProduct( tr.plane.normal, vecNewVelocity ) * tr.plane.normal;
if ( m_nShouldReflect > 0 )
{
// stomp with reflection
vecNewDirVelocity = 2.f * vecNewDirVelocity - vecNewVelocity;
}
// once we change direction, just ignore the attacker vel
pFlame->m_vecAttackerVelocity = vec3_origin;
// update the new velocity and new pos
vecNewVelocity = vecNewDirVelocity;
// set pos to some offset from the surface
vecNewPos = tr.endpos + GetRadius( pFlame ) * tr.plane.normal;
// offset in a new direction
vecNewPos += ( 1.f - tr.fraction ) * flDT * vecNewDirVelocity;
return false;
}
void CTFFlameManager::ModifyAdditionalMovementInfo( tf_point_t *pPoint, float flDT )
{
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
if ( ShouldIgnorePlayerVelocity() )
{
flame_point_t *pFlame = static_cast< flame_point_t * >( pPoint );
float flAttackerSpeed = pFlame->m_vecAttackerVelocity.NormalizeInPlace();
flAttackerSpeed = Clamp( flAttackerSpeed - flDT * GetDrag() * flAttackerSpeed, 0.f, flAttackerSpeed );
pFlame->m_vecAttackerVelocity *= flAttackerSpeed;
}
}
float CTFFlameManager::GetInitialSpeed() const
{
#ifdef WATERFALL_FLAMETHROWER_TEST
if ( m_iWaterfallMode )
{
float flOverride = tf_flame_waterfall_speed_override.GetFloat();
if ( flOverride != 0.f )
{
return flOverride;
}
}
#endif // WATERFALL_FLAMETHROWER_TEST
return m_flFlameSpeed;
}
float CTFFlameManager::GetLifeTime() const
{
float flFlameLifeTime = m_flFlameLifeTime;
#ifdef WATERFALL_FLAMETHROWER_TEST
if ( m_iWaterfallMode )
{
float flOverride = tf_flame_waterfall_lifetime_override.GetFloat();
if ( flOverride != 0.f )
{
flFlameLifeTime = flOverride;
}
}
#endif // WATERFALL_FLAMETHROWER_TEST
return flFlameLifeTime + m_randomStream.RandomFloat( -m_flRandomLifeTimeOffset, m_flRandomLifeTimeOffset );
}
float CTFFlameManager::GetGravity() const
{
#ifdef WATERFALL_FLAMETHROWER_TEST
if ( m_iWaterfallMode )
{
float flOverride = tf_flame_waterfall_gravity_override.GetFloat();
if ( flOverride != 0.f )
{
return flOverride;
}
}
#endif // WATERFALL_FLAMETHROWER_TEST
return m_flFlameGravity;
}
float CTFFlameManager::GetDrag() const
{
#ifdef WATERFALL_FLAMETHROWER_TEST
if ( m_iWaterfallMode )
{
float flOverride = tf_flame_waterfall_drag_override.GetFloat();
if ( flOverride != 0.f )
{
return flOverride;
}
}
#endif // WATERFALL_FLAMETHROWER_TEST
return m_flFlameDrag;
}
Vector CTFFlameManager::GetAdditionalVelocity( const tf_point_t *pPoint ) const
{
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
float flFlameUp = m_flFlameUp;
const flame_point_t *pFlame = static_cast< const flame_point_t * >( pPoint );
// only add attacker vel in the direction of base vel
Vector vecBaseDir = pFlame->m_vecVelocity.Normalized();
Vector vecAttackerVelocity = DotProduct( pFlame->m_vecAttackerVelocity, vecBaseDir ) * vecBaseDir;
return Vector( 0, 0, flFlameUp ) + vecAttackerVelocity;
}
float CTFFlameManager::GetRadius( const tf_point_t *pPoint ) const
{
return tf_flamethrower_boxsize.GetFloat() * GetFlameSizeMult( pPoint );
}