|
|
/**
* Inferno.cpp * An Inferno * Author: Michael S. Booth, February 2005 * Copyright (c) 2005 Turtle Rock Studios, Inc. - All Rights Reserved */
#include "cbase.h"
#include "inferno.h"
#include "engine/IEngineSound.h"
#include "SoundEmitterSystem/isoundemittersystembase.h"
#include <coordsize.h>
#include "tier0/vprof.h"
#include "igameevents.h"
#include "particle_parse.h"
#include "entityutil.h"
#include "func_elevator.h"
#include "nav.h"
#include "nav_mesh.h"
#include "cs_shareddefs.h"
#include "smokegrenade_projectile.h"
#include "improv_locomotor.h"
// NOTE: This has to be the last file included!
#include "tier0/memdbgon.h"
IMPLEMENT_SERVERCLASS_ST( CInferno, DT_Inferno ) SendPropArray3( SENDINFO_ARRAY3(m_fireXDelta), SendPropInt( SENDINFO_ARRAY(m_fireXDelta), COORD_INTEGER_BITS+1, 0 ) ), SendPropArray3( SENDINFO_ARRAY3(m_fireYDelta), SendPropInt( SENDINFO_ARRAY(m_fireYDelta), COORD_INTEGER_BITS+1, 0 ) ), SendPropArray3( SENDINFO_ARRAY3(m_fireZDelta), SendPropInt( SENDINFO_ARRAY(m_fireZDelta), COORD_INTEGER_BITS+1, 0 ) ), SendPropArray3( SENDINFO_ARRAY3(m_bFireIsBurning), SendPropBool( SENDINFO_ARRAY(m_bFireIsBurning) ) ), //SendPropArray3( SENDINFO_ARRAY3(m_BurnNormal), SendPropVector( SENDINFO_NOCHECK( m_BurnNormal ), 0, SPROP_NORMAL ) ),
SendPropInt( SENDINFO(m_fireCount), 7, SPROP_UNSIGNED ), END_SEND_TABLE()
BEGIN_DATADESC( CInferno ) DEFINE_THINKFUNC( InfernoThink ), END_DATADESC()
LINK_ENTITY_TO_CLASS( inferno, CInferno ); PRECACHE_REGISTER( inferno );
IMPLEMENT_SERVERCLASS_ST( CFireCrackerBlast, DT_FireCrackerBlast ) END_SEND_TABLE()
LINK_ENTITY_TO_CLASS( fire_cracker_blast, CFireCrackerBlast ); PRECACHE_REGISTER( fire_cracker_blast );
ConVar InfernoPerFlameSpawnDuration( "inferno_per_flame_spawn_duration", "3", FCVAR_CHEAT, "Duration each new flame will attempt to spawn new flames" ); ConVar InfernoInitialSpawnInterval( "inferno_initial_spawn_interval", "0.02", FCVAR_CHEAT, "Time between spawning flames for first fire" ); ConVar InfernoChildSpawnIntervalMultiplier( "inferno_child_spawn_interval_multiplier", "0.1", FCVAR_CHEAT, "Amount spawn interval increases for each child" ); ConVar InfernoMaxChildSpawnInterval( "inferno_max_child_spawn_interval", "0.5", FCVAR_CHEAT, "Largest time interval for child flame spawning" ); ConVar InfernoSpawnAngle( "inferno_spawn_angle", "45", FCVAR_CHEAT, "Angular change from parent" ); ConVar InfernoMaxFlames( "inferno_max_flames", "16", FCVAR_CHEAT, "Maximum number of flames that can be created" ); ConVar InfernoFlameSpacing( "inferno_flame_spacing", "42", FCVAR_CHEAT, "Minimum distance between separate flame spawns" ); ConVar InfernoFlameLifetime( "inferno_flame_lifetime", "7", FCVAR_CHEAT, "Average lifetime of each flame in seconds" ); ConVar InfernoFriendlyFireDuration( "inferno_friendly_fire_duration", "6", FCVAR_CHEAT, "For this long, FF is credited back to the thrower." ); ConVar InfernoDebug( "inferno_debug", "0", FCVAR_CHEAT ); ConVar InfernoDamage( "inferno_damage", "40", FCVAR_CHEAT, "Damage per second" ); ConVar InfernoMaxRange( "inferno_max_range", "150", FCVAR_CHEAT, "Maximum distance flames can spread from their initial ignition point" ); ConVar InfernoVelocityFactor( "inferno_velocity_factor", "0.003", FCVAR_CHEAT ); ConVar InfernoVelocityDecayFactor( "inferno_velocity_decay_factor", "0.2", FCVAR_CHEAT ); ConVar InfernoVelocityNormalFactor( "inferno_velocity_normal_factor", "0", FCVAR_CHEAT ); ConVar InfernoSurfaceOffset( "inferno_surface_offset", "20", FCVAR_CHEAT ); ConVar InfernoChildSpawnMaxDepth( "inferno_child_spawn_max_depth", "4", FCVAR_CHEAT ); ConVar inferno_scorch_decals( "inferno_scorch_decals", "1", FCVAR_CHEAT ); ConVar inferno_max_trace_per_tick("inferno_max_trace_per_tick", "16"); ConVar inferno_forward_reduction_factor( "inferno_forward_reduction_factor", "0.9", FCVAR_CHEAT );
// Inferno trace masks can allow to do different traces for spreading fire
#define INFERNO_MASK_TO_GROUND ((MASK_SOLID_BRUSHONLY) & (~CONTENTS_GRATE))
#define INFERNO_MASK_LOS_CHECK ( INFERNO_MASK_TO_GROUND | CONTENTS_MONSTER )
#define INFERNO_MASK_DAMAGE INFERNO_MASK_LOS_CHECK
// Smoke grenade radius constant is actually tuned for the bots
// and not for gameplay. Visualizing smoke will show that it goes
// up from the emitter by 128 units (fuzzy top), nothing goes down,
// and it makes a wide XY-donut with a radius of *128* units (fuzzy edges).
ASSERT_INVARIANT( CONSTANT_UNITS_SMOKEGRENADERADIUS == 166 ); // When interacting with fire we don't want any vertical interactions unless
// contact points are definitely in smoke vertically.
static const float SmokeGrenadeRadius_InfernoAffectingZ = 120.0f; // When interacting with fire on the same plane we don't want alpha depth-fighting
// in the most common case, so leave a grace margin between the smoke particles
// and the fire particles.
static const float SmokeGrenadeRadius_InfernoAffectingXY_topedge = 100.0f; static const float SmokeGrenadeRadius_InfernoAffectingXY_equator = 150.0f; static const float SmokeGrenadeRadius_InfernoAffectingXY_bottomedge = 128.0f;
// Fire burning things and smoke constants
static const float InfernoFire_HalfWidth = 30.0f; static const float InfernoFire_FullHeight = 80.0f;
static bool BCheckFirePointInSmokeCloud( const Vector &vecFirePoint, const Vector &vecSmokeOrigin ) { const float flFireUpToSmokeCheckHeight = ( 2 * InfernoFire_HalfWidth + 4.0f ); vec_t flFireAboveSmokeZ = ( vecFirePoint.z - vecSmokeOrigin.z ); if ( flFireAboveSmokeZ < -flFireUpToSmokeCheckHeight ) return false; // fire not tall enough to burn up to smoke
if ( flFireAboveSmokeZ > SmokeGrenadeRadius_InfernoAffectingZ ) return false; // smoke cloud not tall enough to reach to the fire
// Now we know that fire is in XY-slice containing the smoke cloud
// Figure out if we are in the equator XY-plane or in the shrinking edge XY-plane
float flRadiusSquaredTest = SmokeGrenadeRadius_InfernoAffectingXY_equator*SmokeGrenadeRadius_InfernoAffectingXY_equator; if ( flFireAboveSmokeZ > SmokeGrenadeRadius_InfernoAffectingZ * 0.6f ) { float flPctFromEquatorToEdge = RemapValClamped( flFireAboveSmokeZ, SmokeGrenadeRadius_InfernoAffectingZ * 0.6f, SmokeGrenadeRadius_InfernoAffectingZ, 0.0f, 1.0f ); flPctFromEquatorToEdge *= flPctFromEquatorToEdge; // 0.0 still equator; 1.0 edge (squaring makes things feel quadratically closer to equator)
flRadiusSquaredTest = RemapValClamped( flPctFromEquatorToEdge, 0.0f, 1.0f, flRadiusSquaredTest, SmokeGrenadeRadius_InfernoAffectingXY_topedge*SmokeGrenadeRadius_InfernoAffectingXY_topedge ); } else if ( flFireAboveSmokeZ < SmokeGrenadeRadius_InfernoAffectingZ * 0.15f ) { float flPctFromEquatorToEdge = RemapValClamped( flFireAboveSmokeZ, SmokeGrenadeRadius_InfernoAffectingZ * 0.1f, -flFireUpToSmokeCheckHeight, 0.0f, 1.0f ); flPctFromEquatorToEdge *= flPctFromEquatorToEdge; // 0.0 still equator; 1.0 edge (squaring makes things feel quadratically closer to equator)
flRadiusSquaredTest = RemapValClamped( flPctFromEquatorToEdge, 0.0f, 1.0f, flRadiusSquaredTest, SmokeGrenadeRadius_InfernoAffectingXY_bottomedge*SmokeGrenadeRadius_InfernoAffectingXY_bottomedge ); }
// Check if it is within XY-plane radius now
vec_t lenXYsqr = ( vecFirePoint - vecSmokeOrigin ).Length2DSqr(); return lenXYsqr <= flRadiusSquaredTest; }
//------------------------------------------------------------------------------------------
CInferno::CInferno() : m_pWeaponInfo( NULL ) { // Set max flames to default in case the user doesn't ask for
// more or less max flames for this inferno.
SetMaxFlames( InfernoMaxFlames.GetInt() ); ListenForGameEvent( "hegrenade_detonate" ); ListenForGameEvent( "smokegrenade_detonate" ); m_bWasCreatedInSmoke = false; }
//------------------------------------------------------------------------------------------
CInferno::~CInferno() { for ( int i = 0; i < m_fireCount; i++ ) { delete m_fire[i]; m_fire[i] = NULL; }
switch( GetInfernoType() ) { case INFERNO_TYPE_FIRE: case INFERNO_TYPE_INCGREN_FIRE: EmitSound( "Inferno.FadeOut" ); StopSound( "Inferno.Loop" ); break; case INFERNO_TYPE_FIREWORKS: EmitSound( "FireworksCrate.Stop" ); StopSound( "FireworksCrate.Start" ); break; } }
//------------------------------------------------------------------------------------------
void CInferno::Precache( void ) { // extend
BaseClass::Precache();
PrecacheScriptSound( "Inferno.Start" ); PrecacheScriptSound( "Inferno.Start_IncGrenade" ); PrecacheScriptSound( "Inferno.StartSweeten" ); PrecacheScriptSound( "Inferno.Loop" ); PrecacheScriptSound( "Inferno.Fire.Ignite" ); PrecacheScriptSound( "Inferno.FadeOut" );
PrecacheParticleSystem( "extinguish_fire" ); PrecacheParticleSystem( "extinsguish_fire_blastout_01" ); PrecacheScriptSound( "Molotov.Throw" );
PrecacheScriptSound( "FireworksCrate.Start" ); PrecacheScriptSound( "FireworksCrate.Stop" );
if( GetParticleEffectName() != NULL ) { PrecacheParticleSystem( GetParticleEffectName() ); }
if( GetImpactParticleEffectName() != NULL ) { PrecacheParticleSystem( GetImpactParticleEffectName() ); } }
//------------------------------------------------------------------------------------------
void CInferno::Spawn( void ) { m_fireCount = 0;
const float damageRampUpTime = 2.0f; m_damageRampTimer.Start( damageRampUpTime );
SetThink( &CInferno::InfernoThink ); SetNextThink( gpGlobals->curtime );
m_NextSpreadTimer.Start( GetFlameSpreadDelay() );
AddFlag( FL_ONFIRE );
SetInfernoType( INFERNO_TYPE_FIRE ); }
//------------------------------------------------------------------------------------------
float CInferno::GetDamagePerSecond() { return InfernoDamage.GetFloat(); }
//------------------------------------------------------------------------------------------
float CInferno::GetFlameLifetime() const { return InfernoFlameLifetime.GetFloat(); }
void CInferno::FireGameEvent( IGameEvent *event ) { const char *eventname = event->GetName();
// if ( Q_strcmp( "hegrenade_detonate", eventname ) == 0 )
// {
// Vector vecGrenade = Vector( ( float )event->GetInt( "x" ), ( float )event->GetInt( "y" ), ( float )event->GetInt( "z" ) );
// float flGrenadeRadius = HEGrenadeRadius*1.5;
//
// if ((vecGrenade - m_startPos).IsLengthLessThan( flGrenadeRadius*4 ))
// {
// ExtinguishFlamesInSphere( vecGrenade, flGrenadeRadius );
// }
// }
if ( Q_strcmp( "smokegrenade_detonate", eventname ) == 0 ) { Vector vecGrenade = Vector( ( float )event->GetInt( "x" ), ( float )event->GetInt( "y" ), ( float )event->GetInt( "z" ) ); if ((vecGrenade - m_startPos).IsLengthLessThan( SmokeGrenadeRadius*4 )) { int extinguishCount = ExtinguishFlamesAroundSmokeGrenade( vecGrenade );
if ( extinguishCount == m_fireCount ) { CSmokeGrenadeProjectile* pEnt = (CSmokeGrenadeProjectile*)CBaseEntity::Instance( INDEXENT( event->GetInt( "entityid" ) ) ); if ( pEnt ) { // If we were extinguished by a detonating smoke grenade, record it so we can report to OGS
pEnt->m_unOGSExtraFlags |= CBaseCSGrenadeProjectile::GRENADE_EXTINGUISHED_INFERNO; } } } } }
//------------------------------------------------------------------------------------------
/**
* Start the Inferno burning */ void CInferno::StartBurning( const Vector &pos, const Vector &normal, const Vector &velocity, int initialDepth ) { m_startPos.x = pos.x + InfernoSurfaceOffset.GetFloat() * normal.x; m_startPos.y = pos.y + InfernoSurfaceOffset.GetFloat() * normal.y; m_startPos.z = pos.z;
// reflect velocity off of surface
float splash = DotProduct( velocity, normal ); Vector remainder = velocity - normal * splash;
m_splashVelocity = remainder - InfernoVelocityNormalFactor.GetFloat() * normal * splash;
QAngle splashangle; VectorAngles( velocity, splashangle );
if( GetImpactParticleEffectName() != NULL ) { DispatchParticleEffect( GetImpactParticleEffectName(), pos, splashangle ); }
if( InfernoDebug.GetBool() ) { NDebugOverlay::Sphere( pos, 0.5f * InfernoFire_HalfWidth, 0, 255, 0, true, 10.0f); NDebugOverlay::Sphere( m_startPos, 0.5f * InfernoFire_HalfWidth, 255, 255, 0, true, 10.0f); } // create the initial bonfire that begins to spread
if ( k_ECreateFireResult_OK == CreateFire( m_startPos, normal, NULL, initialDepth ) ) { switch( GetInfernoType() ) { case INFERNO_TYPE_FIRE: EmitSound( "Inferno.Start" ); EmitSound( "Inferno.StartSweeten" ); EmitSound( "Inferno.Loop" ); break; case INFERNO_TYPE_INCGREN_FIRE: EmitSound( "Inferno.Start_IncGrenade" ); EmitSound( "Inferno.StartSweeten_IncGrenade" ); EmitSound( "Inferno.Loop" ); break; case INFERNO_TYPE_FIREWORKS: EmitSound( "FireworksCrate.Start" ); break; }
m_startPos = m_fire[0]->m_pos; SetAbsOrigin( m_startPos );
IGameEvent * event = gameeventmanager->CreateEvent( "inferno_startburn" ); if ( event ) { event->SetInt( "entityid", this->entindex() ); event->SetFloat( "x", m_startPos.x ); event->SetFloat( "y", m_startPos.y ); event->SetFloat( "z", m_startPos.z ); gameeventmanager->FireEvent( event ); }
m_activeTimer.Start(); } else { EmitSound( "Molotov.Extinguish" ); DispatchParticleEffect( "extinguish_fire", m_startPos, splashangle ); UTIL_Remove( this ); } }
class CInfernoLOSTraceFilter : public CTraceFilter { // Find which objects can block molotov spreads
virtual bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) { CBaseEntity *pEntity = EntityFromEntityHandle( pHandleEntity );
// Players do not block spread
if ( pEntity->IsPlayer() ) return false; // Chickens, hostages, and other 'navigating' objects don't block spread
if( dynamic_cast< CImprovLocomotor* >( pEntity ) != nullptr ) return false;
// Other objects (doors, terrain, etc.) block spread
return true; } };
//------------------------------------------------------------------------------------------
/**
* Spread the flames */ void CInferno::Spread( const Vector &spreadVelocity ) { if( m_NextSpreadTimer.HasStarted() && !m_NextSpreadTimer.IsElapsed() ) { return; }
m_NextSpreadTimer.Start( GetFlameSpreadDelay() );
for ( int i = 0; i < m_fireCount; i++ ) { // attempt to spawn child-flames
FireInfo *fire = m_fire[ i ]; if ( !fire->m_burning || fire->m_lifetime.IsElapsed() ) continue; // This flame has been extinguished or elapsed, shouldn't be spreading from here
if ( !fire->m_spawnLifetime.IsElapsed() && fire->m_spawnTimer.IsElapsed() ) { fire->m_spawnTimer.Reset(); fire->m_spawnCount++; } }
int traceCount = inferno_max_trace_per_tick.GetInt(); int nextFireOffset = m_fireSpawnOffset + 1; for ( int i = 0; i < m_fireCount && traceCount > 0; i++ ) { if ( m_fireCount >= MIN( ( int ) MAX_INFERNO_FIRES, m_nMaxFlames ) ) break;
int fireIndex = (i + m_fireSpawnOffset) % m_fireCount; FireInfo *fire = m_fire[fireIndex]; nextFireOffset = fireIndex; if ( !fire->m_spawnCount ) continue; if ( !fire->m_burning || fire->m_lifetime.IsElapsed() ) continue; // This flame has been extinguished or elapsed, shouldn't be spreading from here
int depth = fire->m_treeDepth + 1; if ( depth >= InfernoChildSpawnMaxDepth.GetInt() ) continue;
fire->m_spawnCount--;
trace_t tr; const int maxRetry = 4; for( int t=0; t<maxRetry; ++t ) { Vector out;
if (fire->m_parent == NULL) { // initial fire spreads outward in a circle
float angle = random->RandomFloat( -3.14159f, 3.14159f ); out = Vector( cos(angle), sin(angle), 0.0f ); } else { // child flames tend to spread away from their parent
Vector to = fire->m_pos - fire->m_parent->m_pos; to.NormalizeInPlace();
QAngle angles; VectorAngles( to, angles );
angles.y += random->RandomFloat( -InfernoSpawnAngle.GetFloat(), InfernoSpawnAngle.GetFloat() );
AngleVectors( angles, &out ); }
// If we're going into a wall, don't keep trying to spread into a wall the entire lifetime - back off to
// a circular spread at the end.
float velocityDecay = pow( InfernoVelocityDecayFactor.GetFloat(), float(fire->m_treeDepth) ); Vector timeAdjustedSpreadVelocity = spreadVelocity * fire->m_lifetime.GetRemainingRatio() * velocityDecay; out += InfernoVelocityFactor.GetFloat() * timeAdjustedSpreadVelocity;
// put fire on plane of ground
Vector side = CrossProduct( fire->m_normal, out ); out = CrossProduct( side, fire->m_normal );
float range = random->RandomFloat( 50.0f, 75.0f );
Vector pos = fire->m_pos + range * out;
// limit maximum range of spread
Vector fireDir = pos - m_startPos; if ( fireDir.IsLengthGreaterThan( InfernoMaxRange.GetFloat() ) ) { VectorNormalize(fireDir); fireDir *= InfernoMaxRange.GetFloat(); pos = m_startPos + fireDir; }
// dont let flames fall too far
const float maxDrop = 200.0f; Vector endPos = pos; endPos.z = fire->m_pos.z - maxDrop;
// put fire on the ground
UTIL_TraceLine( pos + Vector( 0, 0, 50.0f ), endPos, INFERNO_MASK_TO_GROUND, NULL, COLLISION_GROUP_NONE, &tr ); traceCount--; if (!tr.DidHit()) { if ( InfernoDebug.GetBool() ) { NDebugOverlay::Line( pos + Vector( 0, 0, 50.0f ), endPos, 255, 255, 0, true, 1.0f ); NDebugOverlay::Cross3D( pos, 5, 255, 0, 0, true, 1.0f ); } m_splashVelocity *= inferno_forward_reduction_factor.GetFloat(); continue; } pos.z = tr.endpos.z; Vector normal = tr.plane.normal;
// make sure we dont go through walls
const Vector fireHeight( 0, 0, InfernoFire_HalfWidth ); CInfernoLOSTraceFilter losTraceFilter; UTIL_TraceLine( fire->m_pos + fireHeight, pos + fireHeight, INFERNO_MASK_LOS_CHECK, &losTraceFilter, &tr ); traceCount--; if (tr.fraction < 1.0f) { if ( InfernoDebug.GetBool() ) { NDebugOverlay::Line( fire->m_pos + fireHeight, pos + fireHeight, 255, 0, 0, true, 1.0f ); } m_splashVelocity *= inferno_forward_reduction_factor.GetFloat(); continue; }
ECreateFireResult_t eCreateFireResult = CreateFire( pos, normal, fire, depth ); if ( ( eCreateFireResult == k_ECreateFireResult_OK ) || ( eCreateFireResult == k_ECreateFireResult_LimitExceeded ) ) break; else if ( eCreateFireResult != k_ECreateFireResult_AlreadyOnFire ) m_splashVelocity *= inferno_forward_reduction_factor.GetFloat();
if ( InfernoDebug.GetBool() ) { if ( eCreateFireResult == k_ECreateFireResult_InSmoke ) NDebugOverlay::Line( fire->m_pos + fireHeight, pos + fireHeight, 255, 255, 0, true, 10.0f ); else if ( eCreateFireResult == k_ECreateFireResult_AlreadyOnFire ) NDebugOverlay::Line( fire->m_pos + fireHeight, pos + fireHeight, 255, 100, 100, true, 2.0f ); else NDebugOverlay::Line( fire->m_pos + fireHeight, pos + fireHeight, 255, 100, 0, true, 10.0f ); } } } m_fireSpawnOffset = nextFireOffset + 1; }
//------------------------------------------------------------------------------------------
/**
* Checks whether destination fire point is within a detonated smoke grenade cloud * This is useful to deny molly detonation in smoke, and to deny fire flames spreading into smoke */ bool CInferno::IsFirePosInSmokeCloud( const Vector &pos ) const { const int INFERNO_SEARCH_ENTS = 32; CBaseEntity *pEntities[ INFERNO_SEARCH_ENTS ]; int iNumEntities = UTIL_EntitiesInSphere( pEntities, INFERNO_SEARCH_ENTS, pos, SmokeGrenadeRadius, FL_GRENADE ); for ( int i = 0; i < iNumEntities; i++ ) { CSmokeGrenadeProjectile *pGrenade = dynamic_cast< CSmokeGrenadeProjectile * >( pEntities[ i ] ); if ( pGrenade && pGrenade->m_bDidSmokeEffect && BCheckFirePointInSmokeCloud( pos, pGrenade->GetAbsOrigin() ) ) { return true; } } return false; }
//------------------------------------------------------------------------------------------
/**
* Create an actual fire entity at the given position */ CInferno::ECreateFireResult_t CInferno::CreateFire( const Vector &pos, const Vector &normal, FireInfo *parent, int depth ) { if ( m_fireCount >= MIN( ( int ) MAX_INFERNO_FIRES, m_nMaxFlames ) ) { return k_ECreateFireResult_LimitExceeded; }
if ( IsTouching( pos, pos, NULL ) ) { // we already created a fire here
return k_ECreateFireResult_AlreadyOnFire; }
// if we throw down a molly in the middle of a smoke grenade, DENY!
if( IsFirePosInSmokeCloud( pos ) ) { m_bWasCreatedInSmoke = true; return k_ECreateFireResult_InSmoke; }
if (InfernoDebug.GetBool()) { if (parent) { NDebugOverlay::Line( parent->m_pos, pos, 0, 255, 255, true, 10.0f ); } }
Vector firePos( pos ); bool overWater = false;
trace_t tr; int contents = enginetrace->GetPointContents( pos, MASK_WATER ); if ( contents & ( CONTENTS_WATER | CONTENTS_SLIME ) ) { Vector fireHeight( 0, 0, 30.0f );
int mask = MASK_SOLID_BRUSHONLY | CONTENTS_SLIME | CONTENTS_WATER; UTIL_TraceLine( pos + fireHeight, pos, mask, NULL, COLLISION_GROUP_NONE, &tr ); if ( tr.allsolid ) { return k_ECreateFireResult_AllSolid; } else { firePos = tr.endpos; overWater = true; } }
FireInfo *fire = new FireInfo;
fire->m_pos = firePos; fire->m_center = firePos + Vector( 0, 0, 0.5f * InfernoFire_FullHeight ); fire->m_normal = normal; fire->m_parent = parent; fire->m_treeDepth = depth; fire->m_spawnCount = 0; fire->m_flWaterHeight = firePos.z - pos.z; fire->m_burning = true;
// all control points on the client die down at the same time, so the server needs to match this
if ( m_activeTimer.HasStarted() ) { fire->m_lifetime.Start( GetFlameLifetime() - m_activeTimer.GetElapsedTime() ); } else { fire->m_lifetime.Start( GetFlameLifetime() ); }
if (parent) { fire->m_spawnLifetime.Start( parent->m_spawnLifetime.GetCountdownDuration() );
float duration = InfernoChildSpawnIntervalMultiplier.GetFloat() * parent->m_spawnTimer.GetCountdownDuration(); if (duration > InfernoMaxChildSpawnInterval.GetFloat()) duration = InfernoMaxChildSpawnInterval.GetFloat();
fire->m_spawnTimer.Start( duration ); } else { fire->m_spawnLifetime.Start( InfernoPerFlameSpawnDuration.GetFloat() ); fire->m_spawnTimer.Start( InfernoInitialSpawnInterval.GetFloat() ); }
// keep a simple array of all active fires
m_fire[ m_fireCount ] = fire;
// propogate across the network
// Compute this fire's position relative to the Inferno entity.
Vector vecDelta = fire->m_pos - GetAbsOrigin();
m_fireXDelta.Set( m_fireCount, (int)vecDelta.x ); m_fireYDelta.Set( m_fireCount, (int)vecDelta.y ); m_fireZDelta.Set( m_fireCount, (int)vecDelta.z ); m_bFireIsBurning.Set( m_fireCount, true ); m_BurnNormal.Set( m_fireCount, normal );
++m_fireCount;
RecomputeExtent(); // emit a small flame burst sound
if( GetInfernoType() == INFERNO_TYPE_FIRE || GetInfernoType() == INFERNO_TYPE_INCGREN_FIRE ) { CSoundParameters params; if ( GetParametersForSound( "Inferno.Fire.Ignite", params, NULL ) ) { EmitSound_t ep( params ); ep.m_pOrigin = &fire->m_pos;
CBroadcastRecipientFilter filter; EmitSound( filter, SOUND_FROM_WORLD, ep ); }
if ( inferno_scorch_decals.GetBool() && !overWater ) { trace_t trace; const float dist = 100.0f; Vector dir( 0, 0, -1 ); UTIL_TraceLine( fire->m_pos, fire->m_pos + dir * dist, MASK_OPAQUE, NULL, COLLISION_GROUP_NONE, &trace ); UTIL_DecalTrace( &trace, "MolotovScorch" ); } }
return k_ECreateFireResult_OK; }
void CInferno::ExtinguishIndividualFlameBySmokeGrenade( int iFire, Vector vecStart ) { m_fire[ iFire ]->m_lifetime.Invalidate();
Vector vecAngleAway = m_fire[ iFire ]->m_pos - vecStart; vecAngleAway.NormalizeInPlace();
QAngle angParticle; VectorAngles( vecAngleAway, angParticle );
DispatchParticleEffect( "extinguish_fire", m_fire[ iFire ]->m_pos, angParticle ); }
int CInferno::ExtinguishFlamesAroundSmokeGrenade( Vector vecStart ) { bool bExtinguished = false; bool bCheckDistanceForFlames = true; int nNumExtinguished = 0; // if the radius overlaps the center, extinguish the whole flame
if ( BCheckFirePointInSmokeCloud( m_startPos, vecStart ) ) bCheckDistanceForFlames = false;
for( int i=0; i<m_fireCount; ++i ) { // if this fire just died, propagate over the network
if ( m_fire[i]->m_burning && ( !bCheckDistanceForFlames || BCheckFirePointInSmokeCloud( m_fire[i]->m_pos, vecStart ) ) ) { ExtinguishIndividualFlameBySmokeGrenade( i, vecStart ); bExtinguished = true; nNumExtinguished++; } }
// if we extinguished third or more of our fire, just put out the rest
if ( !bCheckDistanceForFlames && nNumExtinguished >= (m_fireCount/3) ) { for( int i=0; i<m_fireCount; ++i ) { // if this fire just died, propagate over the network
if ( !m_fire[i]->m_lifetime.IsElapsed() ) { ExtinguishIndividualFlameBySmokeGrenade( i, vecStart ); nNumExtinguished++; } } }
if ( bExtinguished ) { EmitSound( "Molotov.Extinguish" );
IGameEvent * event = gameeventmanager->CreateEvent( "inferno_extinguish" ); if ( event ) { event->SetInt( "entityid", this->entindex() ); event->SetFloat( "x", m_startPos.x ); event->SetFloat( "y", m_startPos.y ); event->SetFloat( "z", m_startPos.z ); gameeventmanager->FireEvent( event ); } }
return nNumExtinguished; }
bool CInferno::CheckExpired() { VPROF_BUDGET( "CInferno::CheckExpired (check lifetimes)", "Fire" );
bool bIsAttachedToMovingObject = GetParent() != NULL; Vector vecInfernoOrigin = GetAbsOrigin();
// check lifetime of flames
bool isDone = true; for ( int i = 0; i < m_fireCount; ++i ) { // Already dead.
if ( !m_fire[i]->m_burning ) continue;
// if this fire just died, propagate over the network
if ( m_fire[i]->m_lifetime.IsElapsed() ) { m_fire[i]->m_pos = Vector( 0, 0, 0 ); m_fire[i]->m_burning = false; m_bFireIsBurning.Set( i, false ); continue; }
// still at least one fire alive
isDone = false;
m_fire[i]->m_pos = vecInfernoOrigin; m_fire[i]->m_pos.x += m_fireXDelta[i]; m_fire[i]->m_pos.y += m_fireYDelta[i]; m_fire[i]->m_pos.z += m_fireZDelta[i];
if ( bIsAttachedToMovingObject ) { RecomputeExtent(); }
if ( InfernoDebug.GetBool() ) { NDebugOverlay::Sphere( m_fire[i]->m_pos, 2.0f * InfernoFire_HalfWidth, 255, 100, 0, true, 0.1f ); } }
if ( isDone ) { IGameEvent * event = gameeventmanager->CreateEvent( "inferno_expire" ); if ( event ) { event->SetInt( "entityid", this->entindex() ); event->SetFloat( "x", m_startPos.x ); event->SetFloat( "y", m_startPos.y ); event->SetFloat( "z", m_startPos.z ); gameeventmanager->FireEvent( event ); }
// if all fires have burned out, we're done
UTIL_Remove( this );
// Expired!
return true; }
// Not expired
return false; }
void CInferno::MarkCoveredAreaAsDamaging() { // mark overlapping nav areas as "damaging"
NavAreaCollector overlap;
// bloat extents enough to ensure any non-damaging area is actually safe
// bloat in Z as well to catch nav areas that may be slightly above/below ground
Extent extent = m_extent; float DangerBloat = 32.0f;
Vector dangerBloat( DangerBloat, DangerBloat, DangerBloat ); extent.lo -= dangerBloat; extent.hi += dangerBloat;
//NDebugOverlay::Box( vec3_origin, extent.lo, extent.hi, 0, 255, 0, 10, 0.1f );
TheNavMesh->ForAllAreasOverlappingExtent( overlap, extent );
FOR_EACH_VEC( overlap.m_area, it ) { CNavArea *area = overlap.m_area[it];
if ( IsTouching( area ) ) { area->MarkAsDamaging( 1.0f ); } } }
//------------------------------------------------------------------------------------------
/**
* Spread the flames */ void CInferno::InfernoThink( void ) { VPROF_BUDGET( "CInferno::InfernoThink", "Fire" );
bool bExpiryCheckPerformed = false;
// Run bookkeeping every 0.1s
if ( m_BookkeepingTimer.Interval(0.1f) ) { bExpiryCheckPerformed = true; if ( CheckExpired() ) return;
// the fire grows...
if ( m_fireCount > 0 && m_fireCount < MIN( m_nMaxFlames, ( int )MAX_INFERNO_FIRES ) ) { VPROF_BUDGET( "CInferno::InfernoThink (spread)", "Fire" ); Spread( m_splashVelocity ); }
// Mark area as damaging for bot avoidance
MarkCoveredAreaAsDamaging(); }
#if 0
// Debug draw flame region
NDebugOverlay::Box( vec3_origin, m_extent.lo, m_extent.hi, 255, 255, 255, 10, 0.1f ); #endif
// Deal damage every 0.2s
const float kDamageTimerSeconds = 0.2f; while ( m_damageTimer.RunEvery( kDamageTimerSeconds ) ) { // Note that we run a lot of code in this RunEvery(), but we expect the loop to run 0 or 1 times
// in almost every case, unless this think function somehow got super delayed by the server
if(!bExpiryCheckPerformed) { bExpiryCheckPerformed = true; if ( CheckExpired() ) return; }
VPROF_BUDGET( "CInferno::InfernoThink (damage)", "Fire" );
const int maxVictims = 256; CBaseEntity *damageList[ maxVictims ]; CBaseEntity *owner = GetOwnerEntity(); int damageCount = 0; const float flameRadius = 2.0f * InfernoFire_HalfWidth;
CBaseEntity *list[ maxVictims ]; int count = UTIL_EntitiesInBox( list, maxVictims, m_extent.lo, m_extent.hi, 0 );
for( int i=0; i<count; ++i ) { if (list[i] == NULL || !list[i]->IsAlive() || list[i] == this) continue;
if (IsTouching( list[i], flameRadius, list[i]->IsPlayer() )) { damageList[damageCount] = list[i]; damageCount++; } }
int damageType = GetDamageType(); #if !defined( CSTRIKE15 )
// After the first few seconds of burning, the thrower isn't responsible for teammates who run into the fire.
if ( m_activeTimer.GetElapsedTime() > InfernoFriendlyFireDuration.GetFloat() || owner == NULL ) { damageType |= DMG_BLAMELESS_FRIENDLY_FIRE; // Add in a flag to prevent FF demerits
} #endif
// Note that we expect molotov this value to be an integer (currently it is 40 * 0.2 == 8 damage per tick)
// If molotov DPS changes, we may need to also adjust how often damage is applied.
//
// We could also change this to tick at the exact rate required to deal 1 damage (tick rate = 1 / GetDamagePerSecond())
// at which point we might want to consider optimizing this loop to run a single time and multiply its damage
// by the damage dealt, so that (for example) if molotovs deal 80 dps on a 64 tick server, we only find the targets
// once during the ticks when the molotov deals 2 damage.
float baseDamage = GetDamagePerSecond() * kDamageTimerSeconds; // dmg / sec * sec / tick = dmg / tick
if ( !m_damageRampTimer.IsElapsed() ) { baseDamage *= m_damageRampTimer.GetElapsedRatio(); }
for ( int i = 0; i < damageCount; i++ ) { // damage the victim
CBaseEntity *pEnt = damageList[i]; float damage = baseDamage;
if( CanHarm( pEnt ) ) { CTakeDamageInfo info( this, owner, damage, damageType );
pEnt->TakeDamage( info );
} } }
// Figure out when our next event is and make sure we get a think tick close to it.
float nextThink = m_BookkeepingTimer.GetTargetTime(); nextThink = MIN( nextThink, m_damageTimer.GetTargetTime() ); SetNextThink( nextThink ); }
//-----------------------------------------------------------------------------------------------
void CInferno::RecomputeExtent( void ) { m_extent.lo = Vector( 999999.9f, 999999.9f, 999999.9f ); m_extent.hi = Vector( -999999.9f, -999999.9f, -999999.9f );
for( int i=0; i<m_fireCount; ++i ) { FireInfo *fire = m_fire[i];
if ( fire->m_pos.x - InfernoFire_HalfWidth < m_extent.lo.x ) m_extent.lo.x = fire->m_pos.x - InfernoFire_HalfWidth;
if ( fire->m_pos.x + InfernoFire_HalfWidth > m_extent.hi.x ) m_extent.hi.x = fire->m_pos.x + InfernoFire_HalfWidth;
if ( fire->m_pos.y - InfernoFire_HalfWidth < m_extent.lo.y ) m_extent.lo.y = fire->m_pos.y - InfernoFire_HalfWidth;
if ( fire->m_pos.y + InfernoFire_HalfWidth > m_extent.hi.y ) m_extent.hi.y = fire->m_pos.y + InfernoFire_HalfWidth;
if ( fire->m_pos.z < m_extent.lo.z ) m_extent.lo.z = fire->m_pos.z;
if ( fire->m_pos.z + InfernoFire_FullHeight > m_extent.hi.z ) m_extent.hi.z = fire->m_pos.z + InfernoFire_FullHeight; } }
bool CInferno::BShouldExtinguishSmokeGrenadeBounce( CBaseEntity *entity, Vector &posDropSmoke ) const { const float radius = 2.0f * InfernoFire_HalfWidth; for ( int i = 0; i < m_fireCount; i++ ) { FireInfo *fire = m_fire[ i ]; if ( !fire->m_burning || fire->m_lifetime.IsElapsed() ) continue; // This flame has been extinguished or elapsed, shouldn't cause damage
if ( ( posDropSmoke - fire->m_center ).IsLengthLessThan( radius ) ) { // doublecheck los if required
trace_t tr; const Vector fireHeight( 0, 0, InfernoFire_HalfWidth ); UTIL_TraceLine( fire->m_center + fireHeight, posDropSmoke, INFERNO_MASK_DAMAGE, entity, COLLISION_GROUP_NONE, &tr ); if ( tr.fraction < 1.0f ) UTIL_TraceLine( fire->m_center, posDropSmoke, INFERNO_MASK_DAMAGE, entity, COLLISION_GROUP_NONE, &tr ); if ( tr.fraction == 1.0f ) { if ( InfernoDebug.GetBool() ) { NDebugOverlay::Line( fire->m_center, posDropSmoke, 255, 0, 255, true, 50.2f ); }
return true; } else { if ( InfernoDebug.GetBool() ) { NDebugOverlay::Line( fire->m_center, posDropSmoke, 255, 0, 0, true, 50.2f ); } } } }
return false; }
//-----------------------------------------------------------------------------------------------
/**
* Return true if position is in contact with a fire within the Inferno */ bool CInferno::IsTouching( CBaseEntity *entity, float radius, bool checkLOS ) const { if ( entity != NULL ) { for ( int i = 0; i < m_fireCount; i++ ) { FireInfo *fire = m_fire[i]; if ( !fire->m_burning || fire->m_lifetime.IsElapsed() ) continue; // This flame has been extinguished or elapsed, shouldn't cause damage
// Calculate the nearest point to our potential victim, from our center point
const Vector fireHeight( 0, 0, InfernoFire_HalfWidth );
Vector pos; Vector fireCheck = fire->m_center; if( checkLOS ) fireCheck += fireHeight; entity->CollisionProp()->CalcNearestPoint( fireCheck, &pos );
if ( ( pos - fireCheck ).IsLengthLessThan( radius ) ) { // touching at least one flame
if( checkLOS ) { // doublecheck los if required
trace_t tr; UTIL_TraceLine( fireCheck, pos, INFERNO_MASK_DAMAGE, entity, COLLISION_GROUP_NONE, &tr ); if ( tr.fraction < 1.0f ) { fireCheck = fire->m_center; entity->CollisionProp()->CalcNearestPoint( fireCheck, &pos ); if ( ( pos - fireCheck ).IsLengthLessThan( radius ) ) UTIL_TraceLine( fireCheck, pos, INFERNO_MASK_DAMAGE, entity, COLLISION_GROUP_NONE, &tr ); } if( tr.fraction == 1.0f ) { if( InfernoDebug.GetBool() ) { NDebugOverlay::Line( fire->m_center, pos, 255, 0, 255, true, 50.2f ); }
return true; } else { if( InfernoDebug.GetBool() ) { NDebugOverlay::Line( fire->m_center, pos, 255, 0, 0, true, 50.2f ); } } } else { // los not needed, it's touching
if( InfernoDebug.GetBool() ) { NDebugOverlay::Line( fire->m_center, pos, 255, 0, 255, true, 0.2f ); }
return true; } } } }
return false; }
//-----------------------------------------------------------------------------------------------
/**
* Return true if given ray intersects any fires * TODO: Check LOS if needed. */ bool CInferno::IsTouching( const Vector &from, const Vector &to, Vector *where ) const { for ( int i = 0; i < m_fireCount; i++ ) { FireInfo *fire = m_fire[i]; if ( !fire->m_burning || fire->m_lifetime.IsElapsed() ) continue; // This flame has been extinguished or elapsed, shouldn't be considered touching
Vector pointOnRay; ClosestPointOnRay( m_fire[i]->m_center, from, to, &pointOnRay );
const float radius = 2.0f * InfernoFire_HalfWidth; if ( ( pointOnRay - m_fire[i]->m_center ).IsLengthLessThan( radius ) ) { if ( where ) { *where = pointOnRay; } return true; } }
return false; }
//-----------------------------------------------------------------------------------------------
/**
* Return true if given area overlaps any fires */ bool CInferno::IsTouching( const CNavArea *area ) const { if ( area != NULL ) { float radius = 2.0f * InfernoFire_HalfWidth;
for ( int i = 0; i < m_fireCount; i++ ) { FireInfo *fire = m_fire[ i ]; if ( !fire->m_burning || fire->m_lifetime.IsElapsed() ) continue; // This flame has been extinguished or elapsed, shouldn't be considered touching
Vector close; area->GetClosestPointOnArea( m_fire[i]->m_center, &close );
close.z += m_fire[i]->m_flWaterHeight; // If the inferno was raised above the water, check the nav as if it was in the original pos
if ( ( close - m_fire[i]->m_center ).IsLengthLessThan( radius ) ) { return true; } } } return false; }
//------------------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------------------
void CFireCrackerBlast::Spawn( void ) { BaseClass::Spawn(); SetInfernoType( INFERNO_TYPE_FIREWORKS ); }
|