//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "fire_damage_mgr.h" #include "entity_burn_effect.h" #include "gasoline_blob.h" #include "tf_obj.h" #include "ai_basenpc.h" #include "tf_gamerules.h" #define FIRE_DAMAGE_APPLY_INTERVAL 0.5 // Apply the damage at this interval. #define FIRE_DECAY_END_VALUE 0.00001 // No more damage from fire can be applied to a player per second. #define MAX_FIRE_DAMAGE_PER_SECOND 15 // The fire heat uses exponential decay. It goes from MAX_FIRE_DAMAGE_PER_SECOND to // FIRE_DECAY_END_VALUE in FIRE_DECAY_SECONDS. #define FIRE_DECAY_SECONDS 3 ConVar fire_damageall( "fire_damageall", "0", 0, "Enable fire damaging team members." ); bool CFireDamageMgr::Init() { m_flApplyDamageCountdown = FIRE_DAMAGE_APPLY_INTERVAL; // Fire decays exponentially: B = A * e^(-kt) // So we set B=FIRE_DECAY_END_VALUE, A=flMaxDamagePerSecond, and t=flFireDecaySeconds, then solve for K. m_flMaxDamagePerSecond = MAX_FIRE_DAMAGE_PER_SECOND; m_flDecayConstant = -log( FIRE_DECAY_END_VALUE / m_flMaxDamagePerSecond ) / FIRE_DECAY_SECONDS; return true; } void CFireDamageMgr::AddDamage( CBaseEntity *pTarget, CBaseEntity *pAttacker, float flDamageAccel, bool bMakeBurnEffect ) { FOR_EACH_LL( m_DamageEnts, iDamageEnt ) { CDamageEnt *pEnt = &m_DamageEnts[iDamageEnt]; if ( pEnt->m_hEnt != pTarget ) continue; for ( int i=0; i < pEnt->m_nAttackers; i++ ) { if ( pEnt->m_Attackers[i].m_hAttacker == pAttacker ) { pEnt->m_Attackers[i].m_flVelocity += flDamageAccel * gpGlobals->frametime; return; } } if ( pEnt->m_nAttackers < CDamageEnt::MAX_ATTACKERS ) { // Add a new attacker. pEnt->m_Attackers[pEnt->m_nAttackers].Init( pAttacker, flDamageAccel * gpGlobals->frametime ); ++pEnt->m_nAttackers; return; } else { // No room for more attackers. Warning( "CFireDamageMgr: ran out of attackers\n" ); return; } } // Add a new CDamageEnt. int iNew = m_DamageEnts.AddToTail(); CDamageEnt *pEnt = &m_DamageEnts[iNew]; pEnt->m_hEnt = pTarget; pEnt->m_bWasAlive = pTarget->IsAlive(); pEnt->m_nAttackers = 1; pEnt->m_Attackers[0].Init( pAttacker, flDamageAccel * gpGlobals->frametime ); if ( bMakeBurnEffect ) pEnt->m_pBurnEffect = CEntityBurnEffect::Create( pTarget ); else pEnt->m_pBurnEffect = NULL; } void CFireDamageMgr::RemoveDamageEnt( int iEnt ) { UTIL_Remove( m_DamageEnts[iEnt].m_pBurnEffect ); m_DamageEnts.Remove( iEnt ); } void CFireDamageMgr::FrameUpdatePostEntityThink() { VPROF( "CFireDamageMgr::FrameUpdatePostEntityThink" ); float frametime = gpGlobals->frametime; // Update the damage countdown. m_flApplyDamageCountdown -= gpGlobals->frametime; bool bApplyDamageThisFrame = false; if ( m_flApplyDamageCountdown <= 0 ) { bApplyDamageThisFrame = true; m_flApplyDamageCountdown += FIRE_DAMAGE_APPLY_INTERVAL; } // (-kt) // Figure out how much all the damage decays this frame: e float flFrameDecay = pow( 2.718281828459045235360, -m_flDecayConstant * frametime ); int iNext; for ( int iCur = m_DamageEnts.Head(); iCur != m_DamageEnts.InvalidIndex(); iCur = iNext ) { iNext = m_DamageEnts.Next( iCur ); CDamageEnt *pEnt = &m_DamageEnts[iCur]; // If the entity was dead and is now alive, stop damage to them so their new body doesn't burn. if ( !pEnt->m_hEnt.Get() || ( !pEnt->m_bWasAlive && pEnt->m_hEnt->IsAlive() ) ) { RemoveDamageEnt( iCur ); pEnt = NULL; continue; } pEnt->m_bWasAlive = pEnt->m_hEnt->IsAlive(); // Sum up each attacker's velocity. float flTotalVelocity = 0; for ( int i=0; i < pEnt->m_nAttackers; i++ ) flTotalVelocity += pEnt->m_Attackers[i].m_flVelocity; // Figure out each attacker's contribution. float flContributionPercent[CDamageEnt::MAX_ATTACKERS]; for ( i=0; i < pEnt->m_nAttackers; i++ ) flContributionPercent[i] = pEnt->m_Attackers[i].m_flVelocity / flTotalVelocity; // Decay each attacker's velocity. flTotalVelocity *= flFrameDecay; // Uniformly scale each attacker's velocity down so the sum total doesn't exceed our maximum. float flPercentScale = 1; if ( flTotalVelocity > m_flMaxDamagePerSecond ) flPercentScale = m_flMaxDamagePerSecond / flTotalVelocity; for ( i=0; i < pEnt->m_nAttackers; i++ ) { CDamageAttacker *pAttacker = &pEnt->m_Attackers[i]; pAttacker->m_flVelocity *= flFrameDecay * flPercentScale; bool bEntsValid = (pEnt->m_Attackers[i].m_hAttacker.Get() != NULL); if ( !bEntsValid || pEnt->m_Attackers[i].m_flVelocity <= 0.001 ) { if ( bEntsValid ) ApplyCollectedDamage( pEnt, i ); // Apply the last-remaining damage from this guy. Q_memmove( &pEnt->m_Attackers[i], &pEnt->m_Attackers[i+1], sizeof( pEnt->m_Attackers[0] ) * (pEnt->m_nAttackers-i-1) ); Q_memmove( &flContributionPercent[i], &flContributionPercent[i+1], sizeof( flContributionPercent[0] ) * (pEnt->m_nAttackers-i-1) ); --pEnt->m_nAttackers; if ( pEnt->m_nAttackers == 0 ) { // This ent isn't being damaged anymore. RemoveDamageEnt( iCur ); break; } --i; } // Update their current damage sum and maybe apply the damage. pAttacker->m_flDamageSum += pAttacker->m_flVelocity * frametime; if ( bApplyDamageThisFrame ) { ApplyCollectedDamage( pEnt, i ); } } } } float GetFireDamageScale( CBaseEntity *pEnt ) { // Objects have a lot more health and we want them to take damage faster. if ( dynamic_cast< CBaseObject* >( pEnt ) ) return 4; else return 1; } void CFireDamageMgr::ApplyCollectedDamage( CFireDamageMgr::CDamageEnt *pEnt, int iAttacker ) { CDamageAttacker *pAttacker = &pEnt->m_Attackers[iAttacker]; CTakeDamageInfo info( NULL, pAttacker->m_hAttacker, pAttacker->m_flDamageSum * GetFireDamageScale( pEnt->m_hEnt ), DMG_BURN ); pEnt->m_hEnt->TakeDamage( info ); pAttacker->m_flDamageSum = 0; } // ------------------------------------------------------------------------------------------------ // // Global functions. // ------------------------------------------------------------------------------------------------ // bool IsBurnableEnt( CBaseEntity *pEntity, int iIgnoreTeam ) { if ( pEntity->m_takedamage == DAMAGE_NO ) return false; CGasolineBlob *pBlob = dynamic_cast< CGasolineBlob* >( pEntity ); if ( pBlob ) { return !pBlob->IsLit(); } if ( pEntity->GetTeamNumber() == iIgnoreTeam && !fire_damageall.GetInt() ) { // Don't damage anyone on the pyro's team (including the pyro himself). return false; } // Now only allow specific types of objects to be damaged. if ( dynamic_cast< CBasePlayer* >( pEntity ) || dynamic_cast< CAI_BaseNPC* >( pEntity ) || dynamic_cast< CBaseObject* >( pEntity ) ) { return true; } return false; } int FindBurnableEntsInSphere( CBaseEntity **ents, float *dists, int nMaxEnts, const Vector &vecCenter, float flSearchRadius, CBaseEntity *pOwner ) { Assert( nMaxEnts > 0 ); int nOutEnts = 0; CBaseEntity *pEntity; for ( CEntitySphereQuery sphere( vecCenter, flSearchRadius ); ( pEntity = sphere.GetCurrentEntity() ) != NULL; sphere.NextEntity() ) { if ( !IsBurnableEnt( pEntity, pOwner->GetTeamNumber() ) ) continue; // Make sure it's not blocked. trace_t tr; Vector vCenter = pEntity->WorldSpaceCenter(); UTIL_TraceLine ( vecCenter, vCenter, MASK_SHOT & (~CONTENTS_HITBOX), NULL, COLLISION_GROUP_NONE, &tr ); if ( tr.fraction != 1.0 && tr.m_pEnt != pEntity ) continue; if ( TFGameRules()->IsTraceBlockedByWorldOrShield( vecCenter, vCenter, pOwner, DMG_BURN | DMG_PROBE, &tr ) ) continue; // Make sure it's in range. const Vector &mins = pEntity->WorldAlignMins(); const Vector &maxs = pEntity->WorldAlignMaxs(); float approxTargetRadius = ( Vector( maxs.x, maxs.y, 0 ) - Vector( mins.x, mins.y, 0 )).Length() * 0.5f; float flDistFromCenter = ( vecCenter - tr.endpos ).Length() - approxTargetRadius; ents[nOutEnts] = pEntity; dists[nOutEnts] = flDistFromCenter; nOutEnts++; if ( nOutEnts >= nMaxEnts ) return nOutEnts; } return nOutEnts; } CFireDamageMgr g_FireDamageMgr; CFireDamageMgr* GetFireDamageMgr() { return &g_FireDamageMgr; }