diff --git a/src/game/client/c_baseviewmodel.cpp b/src/game/client/c_baseviewmodel.cpp index 07a1e71c642..4773a44c456 100644 --- a/src/game/client/c_baseviewmodel.cpp +++ b/src/game/client/c_baseviewmodel.cpp @@ -213,6 +213,7 @@ bool C_BaseViewModel::ShouldFlipViewModel() { return pWeapon->m_bFlipViewModel != cl_flipviewmodels.GetBool(); } + return cl_flipviewmodels.GetBool(); // hack for scout ball projeciles to have properly flipped viewmodels #endif return false; diff --git a/src/game/client/tf/tf_hud_itemeffectmeter.cpp b/src/game/client/tf/tf_hud_itemeffectmeter.cpp index ef2e6df8c77..7f8fda038dc 100644 --- a/src/game/client/tf/tf_hud_itemeffectmeter.cpp +++ b/src/game/client/tf/tf_hud_itemeffectmeter.cpp @@ -298,6 +298,7 @@ void CHudItemEffectMeter::CreateHudElementsForClass( C_TFPlayer* pPlayer, CUtlVe } case TF_CLASS_DEMOMAN: DECLARE_ITEM_EFFECT_METER( CTFSword, TF_WEAPON_SWORD, false, "resource/UI/HudItemEffectMeter_Demoman.res" ); + lambdaAddItemEffectMeter("tf_weapon_stickbomb", true); break; case TF_CLASS_SOLDIER: diff --git a/src/game/client/tf/tf_hud_target_id.cpp b/src/game/client/tf/tf_hud_target_id.cpp index a203b5e80ce..ad81b945afb 100644 --- a/src/game/client/tf/tf_hud_target_id.cpp +++ b/src/game/client/tf/tf_hud_target_id.cpp @@ -69,7 +69,7 @@ void DisableFloatingHealthCallback( IConVar *var, const char *oldString, float o pTargetID->InvalidateLayout(); } } -ConVar tf_hud_target_id_disable_floating_health( "tf_hud_target_id_disable_floating_health", "0", FCVAR_ARCHIVE, "Set to disable floating health bar", DisableFloatingHealthCallback ); +ConVar tf_hud_target_id_disable_floating_health( "tf_hud_target_id_disable_floating_health", "0", FCVAR_ARCHIVE, "Set to disable floating health bar. 1 = everyone, 2 = normal players only.", DisableFloatingHealthCallback ); ConVar tf_hud_target_id_alpha( "tf_hud_target_id_alpha", "100", FCVAR_ARCHIVE, "Alpha value of target id background, default 100" ); ConVar tf_hud_target_id_offset( "tf_hud_target_id_offset", "0", FCVAR_ARCHIVE, "RES file Y offset for target id" ); ConVar tf_hud_target_id_show_avatars( "tf_hud_target_id_show_avatars", "1", FCVAR_ARCHIVE, "Display Steam avatars on TargetID when using floating health icons. 1 = everyone, 2 = friends only." ); @@ -80,15 +80,17 @@ bool ShouldHealthBarBeVisible( CBaseEntity *pTarget, CTFPlayer *pLocalPlayer ) if ( !pTarget || !pLocalPlayer ) return false; - if ( tf_hud_target_id_disable_floating_health.GetBool() ) - return false; - - if ( pTarget->IsHealthBarVisible() ) + // now second in priority to force floating health bars on for giant robots in MvM, robot destruction NPCs, and any custom game logic that forces the mini boss flag on players + // regardless of setting due to entities that return this check as true typically not having a visible target ID to fall back to when tf_hud_target_id_disable_floating_health is 1. + if ( pTarget->IsHealthBarVisible() && tf_hud_target_id_disable_floating_health.GetInt() != 1 ) return true; + if ( tf_hud_target_id_disable_floating_health.GetBool() ) + return false; + if ( !pTarget->IsPlayer() ) return false; - + int iHideEnemyHealth = 0; CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pLocalPlayer, iHideEnemyHealth, hide_enemy_health ); if ( ( iHideEnemyHealth > 0 ) && !pLocalPlayer->InSameTeam( pTarget ) ) @@ -475,7 +477,8 @@ bool CTargetID::IsValidIDTarget( int nEntIndex, float flOldTargetRetainFOV, floa //Recreate the floating health icon if there isn't one, we're not a spectator, and // we're not a spy or this was a robot from Robot Destruction-Mode - if ( !m_pFloatingHealthIcon && !bSpectator && ( !bSpy || bHealthBarVisible ) && !DrawHealthIcon() ) + // force render floating health icon if it's a player miniboss or RD robot. + if ( !m_pFloatingHealthIcon && !bSpectator && ( !bSpy || bHealthBarVisible ) && ( !DrawHealthIcon() || pEnt->IsHealthBarVisible() && tf_hud_target_id_disable_floating_health.GetInt() != 1 ) ) { m_pFloatingHealthIcon = CFloatingHealthIcon::AddFloatingHealthIcon( pEnt ); } @@ -794,21 +797,25 @@ void CTargetID::UpdateID( void ) } } + // Remove redundant class checks in favor for the attribute itself, + // and remove second enemy disguised spy check + // This allows for better support ith the see_enemy_health attribute on + // all classes. bool bInSameTeam = pLocalTFPlayer->InSameDisguisedTeam( pEnt ); bool bSpy = pLocalTFPlayer->IsPlayerClass( TF_CLASS_SPY ); - bool bMedic = pLocalTFPlayer->IsPlayerClass( TF_CLASS_MEDIC ); - bool bHeavy = pLocalTFPlayer->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ); + int iSeeEnemyHealth = 0; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pLocalTFPlayer, iSeeEnemyHealth, see_enemy_health ) // See if the player wants to fill in the data string bool bIsAmmoData = false; bool bIsKillStreakData = false; pPlayer->GetTargetIDDataString( bDisguisedTarget, sDataString, sizeof(sDataString), bIsAmmoData, bIsKillStreakData ); - if ( pLocalTFPlayer->GetTeamNumber() == TEAM_SPECTATOR || bInSameTeam || bSpy || bDisguisedEnemy || bMedic || bHeavy ) + if ( pLocalTFPlayer->GetTeamNumber() == TEAM_SPECTATOR || bInSameTeam ) { printFormatString = "#TF_playerid_sameteam"; bShowHealth = true; } - else if ( pLocalTFPlayer->m_Shared.GetState() == TF_STATE_DYING ) + else if ( bSpy || iSeeEnemyHealth || pLocalTFPlayer->m_Shared.GetState() == TF_STATE_DYING ) { // We're looking at an enemy who killed us. printFormatString = "#TF_playerid_diffteam"; diff --git a/src/game/server/tf/tf_obj.cpp b/src/game/server/tf/tf_obj.cpp index 629c807fbda..2a8a665e810 100644 --- a/src/game/server/tf/tf_obj.cpp +++ b/src/game/server/tf/tf_obj.cpp @@ -44,6 +44,7 @@ #include "tf_weapon_wrench.h" #include "tf_weapon_grenade_pipebomb.h" #include "tf_weapon_builder.h" +#include "tf_objective_resource.h" #include "player_vs_environment/tf_population_manager.h" @@ -3105,15 +3106,19 @@ void CBaseObject::UpgradeThink( void ) //----------------------------------------------------------------------------- // Purpose: Handles health upgrade for objects we've already built //----------------------------------------------------------------------------- -void CBaseObject::ApplyHealthUpgrade( void ) +void CBaseObject::ApplyHealthUpgrade(void) { - CTFPlayer *pTFPlayer = GetOwner(); - if ( !pTFPlayer ) + CTFPlayer* pTFPlayer = GetOwner(); + if (!pTFPlayer) return; - int iHealth = GetMaxHealthForCurrentLevel(); - SetMaxHealth( iHealth ); - SetHealth( iHealth ); + int iMaxHealth = GetMaxHealthForCurrentLevel(); + SetMaxHealth(iMaxHealth); + // set health up (or down) to max only between waves in MvM, + // or if current health would end up higher than max health (possible via "unbuying" the building health upgrade). + // fixes "infinite" building health exploit in both cases + if (TFObjectiveResource() && !TFObjectiveResource()->GetMannVsMachineIsBetweenWaves() || GetHealth() > iMaxHealth) + SetHealth(iMaxHealth); //DevMsg( "%i\n", GetMaxHealth() ); } diff --git a/src/game/server/tf/tf_projectile_arrow.cpp b/src/game/server/tf/tf_projectile_arrow.cpp index 6ca38190609..dcef50b2506 100644 --- a/src/game/server/tf/tf_projectile_arrow.cpp +++ b/src/game/server/tf/tf_projectile_arrow.cpp @@ -738,11 +738,70 @@ void CTFProjectile_Arrow::ArrowTouch( CBaseEntity *pOther ) if ( m_bStruckEnemy || (GetMoveType() == MOVETYPE_NONE) ) return; - if ( !pOther ) + if (!pOther) return; - bool bShield = pOther->IsCombatItem() && !InSameTeam( pOther ); - CTFPumpkinBomb *pPumpkinBomb = dynamic_cast< CTFPumpkinBomb * >( pOther ); + // Used when checking against things like FUNC_BRUSHES. + // Copied from CTFProjectile_EnergyRing::ProjectileTouch(), + // but it's needed for penetrating arrows so this will only run when m_bPenetrate is true. + if (m_bPenetrate && !pOther->IsWorld() && pOther->GetSolid() == SOLID_VPHYSICS) + { + const trace_t* pTrace = &CBaseEntity::GetTouchTrace(); + CPhysCollide* pTriggerCollide = modelinfo->GetVCollide(GetModelIndex())->solids[0]; + Assert(pTriggerCollide); + + CUtlVector collideList; + IPhysicsObject* pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int physicsCount = pOther->VPhysicsGetObjectList(pList, ARRAYSIZE(pList)); + vcollide_t* pVCollide = modelinfo->GetVCollide(pOther->GetModelIndex()); + + if (physicsCount) + { + for (int i = 0; i < physicsCount; i++) + { + const CPhysCollide* pCollide = pList[i]->GetCollide(); + if (pCollide) + { + collidelist_t element; + element.pCollide = pCollide; + pList[i]->GetPosition(&element.origin, &element.angles); + collideList.AddToTail(element); + } + } + } + else if (pVCollide && pVCollide->solidCount) + { + collidelist_t element; + element.pCollide = pVCollide->solids[0]; + element.origin = pOther->GetAbsOrigin(); + element.angles = pOther->GetAbsAngles(); + collideList.AddToTail(element); + } + else + { + return; + } + + for (int i = collideList.Count() - 1; i >= 0; --i) + { + const collidelist_t& element = collideList[i]; + trace_t tr; + physcollision->TraceCollide(pTrace->startpos, element.origin, element.pCollide, element.angles, pTriggerCollide, GetAbsOrigin(), GetAbsAngles(), &tr); + if (!tr.DidHit()) + return; + } + if (!pOther->IsSolid() || + pOther->IsSolidFlagSet(FSOLID_VOLUME_CONTENTS) || + (pOther->GetCollisionGroup() == TFCOLLISION_GROUP_RESPAWNROOMS) || + pOther->IsFuncLOD() || + pOther->GetFlags() & FL_WORLDBRUSH) + { + return; + } + } + + bool bShield = pOther->IsCombatItem() && !InSameTeam(pOther); + CTFPumpkinBomb* pPumpkinBomb = dynamic_cast(pOther); if ( pOther->IsSolidFlagSet( FSOLID_TRIGGER | FSOLID_VOLUME_CONTENTS ) && !pPumpkinBomb && !bShield ) return; diff --git a/src/game/shared/tf/halloween/tf_weapon_spellbook.cpp b/src/game/shared/tf/halloween/tf_weapon_spellbook.cpp index fbfb75d5a66..074629930c8 100644 --- a/src/game/shared/tf/halloween/tf_weapon_spellbook.cpp +++ b/src/game/shared/tf/halloween/tf_weapon_spellbook.cpp @@ -939,14 +939,14 @@ void CTFSpellBook::TossJarThink( void ) Vector vecForward, vecRight, vecUp; AngleVectors( pPlayer->EyeAngles(), &vecForward, &vecRight, &vecUp ); - float fRight = 8.f; + float fRight = 7.f; if ( IsViewModelFlipped() ) { fRight *= -1; } Vector vecSrc = pPlayer->Weapon_ShootPosition(); // Make spell toss position at the hand - vecSrc = vecSrc + (vecUp * -9.0f) + (vecRight * 7.0f) + (vecForward * 3.0f); + vecSrc = vecSrc + (vecUp * -9.0f) + (vecRight * fRight) + (vecForward * 3.0f); Vector vecVelocity = GetVelocityVector( vecForward, vecRight, vecUp ) * pSpellData->m_flSpeedScale; QAngle angForward = pPlayer->EyeAngles(); @@ -1417,6 +1417,7 @@ bool CTFSpellBook::CastRocketJump( CTFPlayer *pPlayer ) CTakeDamageInfo info; info.SetAttacker( pPlayer ); info.SetInflictor( pPlayer ); + info.SetWeapon( pPlayer->GetActiveWeapon() ); info.SetDamage( 20.f ); info.SetDamageCustom( TF_DMG_CUSTOM_SPELL_BLASTJUMP ); info.SetDamagePosition( origin ); @@ -1716,6 +1717,8 @@ class CTFProjectile_SpellFireball : public CTFProjectile_Rocket if ( pBaseTarget->GetTeamNumber() == GetTeamNumber() ) return; + CBaseEntity* pInflictor = GetLauncher(); + if ( pTarget ) { if ( pTarget->m_Shared.IsInvulnerable() ) @@ -1724,13 +1727,13 @@ class CTFProjectile_SpellFireball : public CTFProjectile_Rocket if ( pTarget->m_Shared.InCond( TF_COND_PHASE ) || pTarget->m_Shared.InCond( TF_COND_PASSTIME_INTERCEPTION ) ) return; - pTarget->m_Shared.SelfBurn( 5.0f ); + // self burn from an enemy projectile just feels wrong. credit the damager properly + pTarget->m_Shared.Burn( pThrower, dynamic_cast( pInflictor ), 5.0f ); } const trace_t *pTrace = &CBaseEntity::GetTouchTrace(); trace_t *pNewTrace = const_cast( pTrace ); - CBaseEntity *pInflictor = GetLauncher(); CTakeDamageInfo info; info.SetAttacker( pThrower ); info.SetInflictor( this ); diff --git a/src/game/shared/tf/tf_dropped_weapon.cpp b/src/game/shared/tf/tf_dropped_weapon.cpp index 2479acbb145..fd316d5c9cf 100644 --- a/src/game/shared/tf/tf_dropped_weapon.cpp +++ b/src/game/shared/tf/tf_dropped_weapon.cpp @@ -572,19 +572,21 @@ void CTFDroppedWeapon::InitDroppedWeapon( CTFPlayer *pPlayer, CTFWeaponBase *pWe } } - if ( bIsSuicide ) - { - m_flChargeLevel = 0.f; - } - else + + CWeaponMedigun* pMedigun = dynamic_cast(pWeapon); + if (pMedigun) { - CWeaponMedigun *pMedigun = dynamic_cast< CWeaponMedigun* >( pWeapon ); - if ( pMedigun ) + SetBodygroup(1, 1); // previously from when weapons were ammo boxes- removes medigun hose + if (bIsSuicide) + { + m_flChargeLevel = 0.f; + } + else { - m_flChargeLevel.Set( pMedigun->GetChargeLevel() ); - if ( m_flChargeLevel > 0.f ) + m_flChargeLevel.Set(pMedigun->GetChargeLevel()); + if (m_flChargeLevel > 0.f) { - SetContextThink( &CTFDroppedWeapon::ChargeLevelDegradeThink, gpGlobals->curtime + 0.1f, "ChargeLevelDegradeThink" ); + SetContextThink(&CTFDroppedWeapon::ChargeLevelDegradeThink, gpGlobals->curtime + 0.1f, "ChargeLevelDegradeThink"); } } } diff --git a/src/game/shared/tf/tf_gamerules.cpp b/src/game/shared/tf/tf_gamerules.cpp index cc43b0f42ac..aff16df1069 100644 --- a/src/game/shared/tf/tf_gamerules.cpp +++ b/src/game/shared/tf/tf_gamerules.cpp @@ -5679,7 +5679,8 @@ void CTFGameRules::RadiusDamage( CTFRadiusDamageInfo &info ) //----------------------------------------------------------------------------- void CTFRadiusDamageInfo::CalculateFalloff( void ) { - if ( dmgInfo->GetDamageType() & DMG_RADIUS_MAX ) + // hack to ignore caber explosion, otherwise the charge minicrit explosion would actually do less damage + if (dmgInfo->GetDamageCustom() != TF_DMG_CUSTOM_STICKBOMB_EXPLOSION && dmgInfo->GetDamageType() & DMG_RADIUS_MAX) flFalloff = 0.f; else if ( dmgInfo->GetDamageType() & DMG_HALF_FALLOFF ) flFalloff = 0.5f; @@ -6532,6 +6533,10 @@ bool CTFGameRules::ApplyOnDamageModifyRules( CTakeDamageInfo &info, CBaseEntity int iForceCritDmgFalloff = 0; CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iForceCritDmgFalloff, crit_dmg_falloff ); + // Move this higher to fix the lack of minicrit rampup damage upon being crit boosted. + int iDemoteCritToMinicrit = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iDemoteCritToMinicrit, crits_become_minicrits ); + #ifdef MCOMS_BALANCE_PACK // SMG headshots falloff if ( pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_SMG ) @@ -6551,7 +6556,7 @@ bool CTFGameRules::ApplyOnDamageModifyRules( CTakeDamageInfo &info, CBaseEntity ( ( bCrit && tf_weapon_criticals_distance_falloff.GetBool() ) || ( info.GetCritType() == CTakeDamageInfo::CRIT_MINI && tf_weapon_minicrits_distance_falloff.GetBool() ) || ( iForceCritDmgFalloff ) ); - bool bDoShortRangeDistanceIncrease = !bCrit || info.GetCritType() == CTakeDamageInfo::CRIT_MINI ; + bool bDoShortRangeDistanceIncrease = !bCrit || iDemoteCritToMinicrit != 0 || info.GetCritType() == CTakeDamageInfo::CRIT_MINI ; bool bDoLongRangeDistanceDecrease = !bIgnoreLongRangeDmgEffects && ( bForceCritFalloff || ( !bCrit && info.GetCritType() != CTakeDamageInfo::CRIT_MINI ) ); // If we're doing any distance modification, we need to do that first @@ -6781,8 +6786,6 @@ bool CTFGameRules::ApplyOnDamageModifyRules( CTakeDamageInfo &info, CBaseEntity } else if ( bCrit ) { - int iDemoteCritToMinicrit = 0; - CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iDemoteCritToMinicrit, crits_become_minicrits ); if ( iDemoteCritToMinicrit != 0 ) { bitsDamage &= ~DMG_CRITICAL; // this is to shutup the assert in lambdaDoMinicrit diff --git a/src/game/shared/tf/tf_item_wearable.cpp b/src/game/shared/tf/tf_item_wearable.cpp index 2a1054208f5..84b8b60019d 100644 --- a/src/game/shared/tf/tf_item_wearable.cpp +++ b/src/game/shared/tf/tf_item_wearable.cpp @@ -812,6 +812,34 @@ ShadowType_t CTFWearableVM::ShadowCastType( void ) return SHADOWS_RENDER_TO_TEXTURE; } + +//----------------------------------------------------------------------------- +// Purpose: +// Allow botkiller attachments and server mod viewmodel VMs to properly flip with the player's weapon. +//----------------------------------------------------------------------------- +int CTFWearableVM::InternalDrawModel(int flags) +{ + C_TFPlayer* pOwner = ToTFPlayer(GetOwnerEntity()); + + CMatRenderContextPtr pRenderContext(materials); + + if (pOwner) + { + CTFWeaponBase* pWpn = pOwner->GetActiveTFWeapon(); + + if (pWpn) + { + if (pWpn->IsViewModelFlipped()) + pRenderContext->CullMode(MATERIAL_CULLMODE_CW); + } + } + + int ret = BaseClass::InternalDrawModel(flags); + + pRenderContext->CullMode(MATERIAL_CULLMODE_CCW); + + return ret; +} #endif diff --git a/src/game/shared/tf/tf_item_wearable.h b/src/game/shared/tf/tf_item_wearable.h index f89c0c956a9..201acf4ec9b 100644 --- a/src/game/shared/tf/tf_item_wearable.h +++ b/src/game/shared/tf/tf_item_wearable.h @@ -109,6 +109,8 @@ class CTFWearableVM : public CTFWearable #if defined( CLIENT_DLL ) virtual ShadowType_t ShadowCastType( void ); + + virtual int InternalDrawModel(int flags); #endif }; diff --git a/src/game/shared/tf/tf_projectile_dragons_fury.cpp b/src/game/shared/tf/tf_projectile_dragons_fury.cpp index cb247eb0435..8027ade3ef5 100644 --- a/src/game/shared/tf/tf_projectile_dragons_fury.cpp +++ b/src/game/shared/tf/tf_projectile_dragons_fury.cpp @@ -181,6 +181,7 @@ class CTFProjectile_BallOfFire : public CTFProjectile_Rocket pOther->IsSolidFlagSet( FSOLID_NOT_SOLID ) || ( pOther->GetCollisionGroup() == TFCOLLISION_GROUP_RESPAWNROOMS ) || pOther->IsFuncLOD() || +// pOther->GetFlags() & FL_WORLDBRUSH && !pOther->IsWorld() || // hack for func_brushes pOther->IsBaseProjectile() ) { return; diff --git a/src/game/shared/tf/tf_projectile_energy_ring.cpp b/src/game/shared/tf/tf_projectile_energy_ring.cpp index d4229ac82fa..a53cd15f565 100644 --- a/src/game/shared/tf/tf_projectile_energy_ring.cpp +++ b/src/game/shared/tf/tf_projectile_energy_ring.cpp @@ -222,7 +222,8 @@ void CTFProjectile_EnergyRing::ProjectileTouch( CBaseEntity *pOther ) !pOther->IsSolid() || pOther->IsSolidFlagSet( FSOLID_VOLUME_CONTENTS ) || ( pOther->GetCollisionGroup() == TFCOLLISION_GROUP_RESPAWNROOMS ) || - pOther->IsFuncLOD() ) + pOther->IsFuncLOD() || + pOther->GetFlags() & FL_WORLDBRUSH) // Hack for func_brushes { return; } @@ -257,7 +258,8 @@ void CTFProjectile_EnergyRing::ProjectileTouch( CBaseEntity *pOther ) if ( bCombatEntity ) { // Bison projectiles shouldn't collide with friendly things - if ( ShouldPenetrate() && ( pOther->InSameTeam( this ) || ( gpGlobals->curtime - m_flLastHitTime ) < tf_bison_tick_time.GetFloat() ) ) + // Pomson projectiles shouldn't collide with nearby teammates + if ( ( ShouldPenetrate() || !CanCollideWithTeammates() ) && ( pOther->InSameTeam( this ) || ( gpGlobals->curtime - m_flLastHitTime ) < tf_bison_tick_time.GetFloat() ) ) return; m_flLastHitTime = gpGlobals->curtime; diff --git a/src/game/shared/tf/tf_projectile_nail.cpp b/src/game/shared/tf/tf_projectile_nail.cpp index f944acfba79..e806211d9f4 100644 --- a/src/game/shared/tf/tf_projectile_nail.cpp +++ b/src/game/shared/tf/tf_projectile_nail.cpp @@ -55,6 +55,13 @@ CTFBaseProjectile *CTFProjectile_Syringe::Create( CBaseEntity *pScorer /*= NULL*/, bool bCritical /*= false */ ) { + float flVelocity = SYRINGE_VELOCITY; + if (pLauncher) // customizable syringe spread and range for server mods. + { + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER(pLauncher, flVelocity, mult_projectile_speed); + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER(pLauncher, flVelocity, mult_projectile_range); + } + return CTFBaseProjectile::Create( "tf_projectile_syringe", vecOrigin, vecAngles, pOwner, SYRINGE_VELOCITY, g_sModelIndexSyringe, SYRINGE_DISPATCH_EFFECT, pScorer, bCritical ); } diff --git a/src/game/shared/tf/tf_weapon_bat.cpp b/src/game/shared/tf/tf_weapon_bat.cpp index e159a7e7506..b1e6d3e9f0d 100644 --- a/src/game/shared/tf/tf_weapon_bat.cpp +++ b/src/game/shared/tf/tf_weapon_bat.cpp @@ -29,7 +29,8 @@ #endif const float DEFAULT_ORNAMENT_EXPLODE_RADIUS = 50.0f; -const float DEFAULT_ORNAMENT_EXPLODE_DAMAGE_MULT = 0.9f; +const float DEFAULT_ORNAMENT_EXPLODE_DAMAGE_MULT = 1.8f; // this for some reason explodes twice in vanilla if it hits a player, so compensate damage here +//const float DEFAULT_ORNAMENT_EXPLODE_DAMAGE_MULT = 0.9f; //============================================================================= // @@ -952,7 +953,8 @@ bool CTFStunBall::ShouldBallTouch( CBaseEntity *pOther ) if ( !pOther || !pOther->IsSolid() || pOther->IsSolidFlagSet( FSOLID_VOLUME_CONTENTS ) || - pOther->GetCollisionGroup() == TFCOLLISION_GROUP_RESPAWNROOMS ) + pOther->GetCollisionGroup() == TFCOLLISION_GROUP_RESPAWNROOMS || + pOther->GetFlags() & FL_WORLDBRUSH ) { pOwner->SpeakConceptIfAllowed( MP_CONCEPT_BALL_MISSED ); return false; @@ -1003,6 +1005,14 @@ void CTFStunBall::IncrementDeflected(void) CreateBallTrail(); } +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFStunBall::DetonateThink(void) +{ + // nothing here, so the ball never fizzles out +} + // -- SERVER ONLY #endif @@ -1071,6 +1081,28 @@ void CTFStunBall::CreateTrailParticles( void ) } } } + +//----------------------------------------------------------------------------- +// Purpose: Removes particles as projectile now simply fades out instead of instnatly deleting itself +//----------------------------------------------------------------------------- +void CTFStunBall::OnDataChanged(DataUpdateType_t updateType) +{ + BaseClass::OnDataChanged(updateType); + + // Remove normal effect if we're inactive + if (GetMoveType() == MOVETYPE_NONE && pEffectTrail) + { + ParticleProp()->StopEmission(pEffectTrail); + pEffectTrail = NULL; + } + + // Remove crit effect if we're inactive + if (GetMoveType() == MOVETYPE_NONE && pEffectCrit) + { + ParticleProp()->StopEmission(pEffectCrit); + pEffectCrit = NULL; + } +} #endif @@ -1236,12 +1268,29 @@ void CTFBall_Ornament::ApplyBallImpactEffectOnVictim( CBaseEntity *pOther ) pPlayer->DispatchTraceAttack( info, dir, pNewTrace ); ApplyMultiDamage(); - // the ball shatters - UTIL_Remove( this ); + // the ball shatters, but the entity is kept for a few seonds for a little bit while the trail finishes. + FadeOut( 1.5 ); m_bTouched = true; } +void CTFBall_Ornament::FadeOut(int iTime) +{ + SetMoveType(MOVETYPE_NONE); + SetAbsVelocity(vec3_origin); + AddSolidFlags(FSOLID_NOT_SOLID); + AddEffects(EF_NODRAW); + + + // Start remove timer. + SetContextThink(&CTFBall_Ornament::RemoveThink, gpGlobals->curtime + iTime, "ORNAMENT_REMOVE_THINK"); +} + +void CTFBall_Ornament::RemoveThink(void) +{ + UTIL_Remove(this); +} + //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- @@ -1250,6 +1299,9 @@ void CTFBall_Ornament::PipebombTouch( CBaseEntity *pOther ) if ( !ShouldBallTouch( pOther ) ) return; + if ( (GetMoveType() == MOVETYPE_NONE)) + return; + trace_t pTrace; Vector velDir = GetAbsVelocity(); VectorNormalize( velDir ); @@ -1259,8 +1311,17 @@ void CTFBall_Ornament::PipebombTouch( CBaseEntity *pOther ) if ( pOther == GetThrower() ) return; + // Don't collide with teammates if we're still in the grace period. + if (pOther->IsPlayer() && InSameTeam(pOther) && !CanCollideWithTeammates()) + return; + // Explode (does radius damage, triggers particles and sound effects). - Explode( &pTrace, DMG_BLAST|DMG_PREVENT_PHYSICS_FORCE ); + int iDmgType = DMG_BLAST | DMG_PREVENT_PHYSICS_FORCE; + if ( IsCritical() ) + { + iDmgType |= DMG_CRITICAL; + } + Explode( &pTrace, iDmgType); if ( !InSameTeam( pOther ) && pOther->m_takedamage != DAMAGE_NO ) { @@ -1301,7 +1362,12 @@ void CTFBall_Ornament::VPhysicsCollisionThink( void ) Vector vecSpot = GetAbsOrigin() - velDir * 16; UTIL_TraceLine( vecSpot, vecSpot + velDir * 32, MASK_SOLID, this, COLLISION_GROUP_NONE, &pTrace ); - Explode( &pTrace, DMG_BLAST|DMG_PREVENT_PHYSICS_FORCE ); + int iDmgType = DMG_BLAST | DMG_PREVENT_PHYSICS_FORCE; + if ( IsCritical() ) + { + iDmgType |= DMG_CRITICAL; + } + Explode( &pTrace, iDmgType ); } //----------------------------------------------------------------------------- @@ -1309,6 +1375,12 @@ void CTFBall_Ornament::VPhysicsCollisionThink( void ) //----------------------------------------------------------------------------- void CTFBall_Ornament::Explode( trace_t *pTrace, int bitsDamageType ) { + // ornament is already expiring, don't explode again + // this seems to lower damage if the ball directly hits a player, as the bauble would actually explode twice before this fix + // this behavior seemed unintentional, but it ultimately ends up with the weapon dealing 4-13 less damage. too bad! + if (GetMoveType() == MOVETYPE_NONE) + return; + // Create smashed glass particles when we explode if ( GetTeamNumber() == TF_TEAM_RED ) { @@ -1342,8 +1414,8 @@ void CTFBall_Ornament::Explode( trace_t *pTrace, int bitsDamageType ) CTFRadiusDamageInfo radiusinfo( &info, vecOrigin, DEFAULT_ORNAMENT_EXPLODE_RADIUS, nullptr, 0.0f, 0.0f ); TFGameRules()->RadiusDamage( radiusinfo ); - UTIL_Remove( this ); + if ( GetMoveType() != MOVETYPE_NONE ) + FadeOut( 1.5 ); } - #endif diff --git a/src/game/shared/tf/tf_weapon_bat.h b/src/game/shared/tf/tf_weapon_bat.h index cfd2fd4e165..9880f725118 100644 --- a/src/game/shared/tf/tf_weapon_bat.h +++ b/src/game/shared/tf/tf_weapon_bat.h @@ -150,6 +150,7 @@ class CTFStunBall : public CTFGrenadePipebombProjectile virtual bool IsAllowedToExplode( void ) OVERRIDE { return false; } virtual void Explode( trace_t *pTrace, int bitsDamageType ); + virtual void DetonateThink() OVERRIDE; virtual void ApplyBallImpactEffectOnVictim( CBaseEntity *pOther ); virtual void PipebombTouch( CBaseEntity *pOther ); virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); @@ -178,6 +179,7 @@ class CTFStunBall : public CTFGrenadePipebombProjectile #else virtual const char *GetTrailParticleName( void ); virtual void CreateTrailParticles( void ); + virtual void OnDataChanged(DataUpdateType_t updateType); #endif @@ -236,6 +238,11 @@ class CTFBall_Ornament : public CTFStunBall virtual void ApplyBallImpactEffectOnVictim( CBaseEntity *pOther ); + void FadeOut(int iTime); + void RemoveThink(); + + virtual bool IsDeflectable() OVERRIDE { return (GetMoveType() != MOVETYPE_NONE); } + protected: Vector m_vCollisionVelocity; #endif diff --git a/src/game/shared/tf/tf_weapon_bottle.cpp b/src/game/shared/tf/tf_weapon_bottle.cpp index 308224f8368..bc5712ee122 100644 --- a/src/game/shared/tf/tf_weapon_bottle.cpp +++ b/src/game/shared/tf/tf_weapon_bottle.cpp @@ -225,6 +225,11 @@ void CTFStickBomb::Smack( void ) { m_iDetonated = 1; m_bBroken = true; + CTFPlayer* pOwner = GetTFPlayerOwner(); + if (pOwner) + { + pOwner->m_Shared.SetItemChargeMeter(LOADOUT_POSITION_MELEE, 0.f); + } SwitchBodyGroups(); #ifdef GAME_DLL @@ -256,6 +261,8 @@ void CTFStickBomb::Smack( void ) int dmgType = DMG_BLAST | DMG_NOCLOSEDISTANCEMOD; if ( IsCurrentAttackACrit() ) dmgType |= DMG_CRITICAL; + else if (m_bMiniCrit) // the explosion minicrits from the charge minicrit, too + dmgType |= DMG_RADIUS_MAX; CTakeDamageInfo info( pTFPlayer, pTFPlayer, this, explosion, explosion, 75.0f, dmgType, TF_DMG_CUSTOM_STICKBOMB_EXPLOSION, &explosion ); CTFRadiusDamageInfo radiusinfo( &info, explosion, 100.f ); @@ -345,3 +352,12 @@ void RecvProxy_Detonated( const CRecvProxyData *pData, void *pStruct, void *pOut } #endif + +void CTFStickBomb::OnResourceMeterFilled() +{ + CTFPlayer* pOwner = GetTFPlayerOwner(); + if (!pOwner) + return; + + WeaponRegenerate(); +} \ No newline at end of file diff --git a/src/game/shared/tf/tf_weapon_bottle.h b/src/game/shared/tf/tf_weapon_bottle.h index 1c886d8bf58..21d003811da 100644 --- a/src/game/shared/tf/tf_weapon_bottle.h +++ b/src/game/shared/tf/tf_weapon_bottle.h @@ -102,6 +102,7 @@ class CTFStickBomb : public CTFBreakableMelee void SetDetonated( int iVal ) { m_iDetonated = iVal; } int GetDetonated( void ) { return m_iDetonated; } + virtual void OnResourceMeterFilled() OVERRIDE; private: diff --git a/src/game/shared/tf/tf_weapon_dragons_fury.cpp b/src/game/shared/tf/tf_weapon_dragons_fury.cpp index c5d50367197..677442c77a4 100644 --- a/src/game/shared/tf/tf_weapon_dragons_fury.cpp +++ b/src/game/shared/tf/tf_weapon_dragons_fury.cpp @@ -109,6 +109,8 @@ void CTFWeaponFlameBall::PrimaryAttack( void ) #else C_CTF_GameStats.Event_PlayerFiredWeapon( pPlayer, IsCurrentAttackACrit() ); #endif + // the DF does not list a "no random crits" stat, therefore it should random crit + CalcIsAttackCritical(); SendWeaponAnim( ACT_VM_PRIMARYATTACK ); @@ -147,14 +149,14 @@ CBaseEntity* CTFWeaponFlameBall::FireProjectile( CTFPlayer *pPlayer ) Vector vecForward, vecRight, vecUp; AngleVectors( pPlayer->EyeAngles(), &vecForward, &vecRight, &vecUp ); - float fRight = 8.f; + float fRight = 7.f; if ( IsViewModelFlipped() ) { fRight *= -1; } Vector vecSrc = pPlayer->Weapon_ShootPosition(); // Shoot from the right location - vecSrc = vecSrc + (vecUp * -9.0f) + (vecRight * 7.0f) + (vecForward * 3.0f); + vecSrc = vecSrc + (vecUp * -9.0f) + (vecRight * fRight) + (vecForward * 3.0f); QAngle angForward = pPlayer->EyeAngles(); @@ -180,7 +182,7 @@ CBaseEntity* CTFWeaponFlameBall::FireProjectile( CTFPlayer *pPlayer ) pRocket->SetDamage( 20 ); pRocket->ChangeTeam( pPlayer->GetTeamNumber() ); - pRocket->SetCritical( pPlayer->m_Shared.IsCritBoosted() ); + pRocket->SetCritical( IsCurrentAttackACrit() ); DispatchSpawn( pRocket ); diff --git a/src/game/shared/tf/tf_weapon_grenade_pipebomb.cpp b/src/game/shared/tf/tf_weapon_grenade_pipebomb.cpp index 9c7f1f68d86..48331e5cebb 100644 --- a/src/game/shared/tf/tf_weapon_grenade_pipebomb.cpp +++ b/src/game/shared/tf/tf_weapon_grenade_pipebomb.cpp @@ -781,6 +781,7 @@ void CTFGrenadePipebombProjectile::PipebombTouch( CBaseEntity *pOther ) // Impact damage scales with distance float flDistanceSq = (pOther->GetAbsOrigin() - pAttacker->GetAbsOrigin()).LengthSqr(); float flImpactDamage = RemapValClamped( flDistanceSq, 512 * 512, 1024 * 1024, 50, 25 ); + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER(GetLauncher(), flImpactDamage, mult_dmg); CTakeDamageInfo info( this, pAttacker, GetOriginalLauncher(), vec3_origin, vOrigin, flImpactDamage, GetDamageType(), TF_DMG_CUSTOM_CANNONBALL_PUSH ); pOther->TakeDamage( info ); diff --git a/src/game/shared/tf/tf_weapon_jar.cpp b/src/game/shared/tf/tf_weapon_jar.cpp index 2f071c0cee5..0346132d4ba 100644 --- a/src/game/shared/tf/tf_weapon_jar.cpp +++ b/src/game/shared/tf/tf_weapon_jar.cpp @@ -11,8 +11,10 @@ // Client specific. #ifdef CLIENT_DLL #include "c_tf_player.h" +#include "c_basedoor.h" // Server specific. #else +#include "doors.h" #include "soundent.h" #include "te_effect_dispatch.h" #include "tf_player.h" @@ -539,7 +541,10 @@ void CTFProjectile_Jar::PipebombTouch( CBaseEntity *pOther ) { // Exception to this rule - if we're a jar or milk, and our potential victim is on fire, then allow collision after all. // If we're a jar or milk, then still allow collision if our potential victim is on fire. - if (m_iProjectileType == TF_PROJECTILE_JAR || m_iProjectileType == TF_PROJECTILE_JAR_MILK) + // This condition only applies to non-cleaver jars, since there's so many jar types capable of extinguishing. + if ( m_iProjectileType != TF_PROJECTILE_CLEAVER && + m_iProjectileType != TF_PROJECTILE_SPELL && + m_iProjectileType != TF_PROJECTILE_THROWABLE) { auto victim = ToTFPlayer(pOther); if (!victim->m_Shared.InCond(TF_COND_BURNING)) @@ -943,8 +948,40 @@ CTFProjectile_Jar *CTFCleaver::CreateJarProjectile( const Vector &position, cons { return CTFProjectile_Cleaver::Create( position, angles, velocity, angVelocity, pOwner, weaponInfo, GetSkin() ); } + #endif +//----------------------------------------------------------------------------- +// Purpose: Determines if there is space to create a cleaver. +// The player technically has a 0.1s window between this passing through and then aiming against the wall +// to get the cleaver through the it, but this is difficult to do so this bug has been effectively mitigated. +//----------------------------------------------------------------------------- +bool CTFCleaver::CanCreateCleaver(CTFPlayer* pPlayer) +{ + Vector vecForward, vecUp; + AngleVectors(pPlayer->EyeAngles(), &vecForward, NULL, &vecUp); + Vector vecCleaverStart = pPlayer->GetAbsOrigin() + Vector(0, 0, 50); + Vector vecCleaverlEnd = vecCleaverStart + vecForward * 32.f; + + // Trace out and see if we hit a wall. + trace_t trace; + CTraceFilterSimple traceFilter(this, COLLISION_GROUP_NONE); + UTIL_TraceHull(vecCleaverStart, vecCleaverlEnd, -Vector(8, 8, 8), Vector(8, 8, 8), MASK_SOLID_BRUSHONLY, &traceFilter, &trace); + if (trace.DidHitWorld() || trace.startsolid) + return false; + else + { + if (trace.m_pEnt) + { + // Don't let the player throw the cleaver through doors. + CBaseDoor* pDoor = dynamic_cast(trace.m_pEnt); + if (pDoor) + return false; + } + return true; + } +} + //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- @@ -953,6 +990,23 @@ float CTFCleaver::GetProjectileSpeed( void ) return TF_CLEAVER_LAUNCH_SPEED; } +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFCleaver::PrimaryAttack(void) +{ + CTFPlayer* pPlayer = GetTFPlayerOwner(); + if (!pPlayer) + return; + + if ( CanCreateCleaver(pPlayer) ) + return BaseClass::PrimaryAttack(); + + return; +} + +//----------------------------------------------------------------------------- +// Purpose: //----------------------------------------------------------------------------- void CTFCleaver::SecondaryAttack( void ) { @@ -1131,6 +1185,14 @@ void CTFProjectile_Cleaver::Detonate( void ) Explode( &tr, GetDamageType() ); } +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFProjectile_Cleaver::DetonateThink(void) +{ + // nothing here, so the cleaver never fizzles out +} + //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- diff --git a/src/game/shared/tf/tf_weapon_jar.h b/src/game/shared/tf/tf_weapon_jar.h index f95aadc4342..b41eb8e0272 100644 --- a/src/game/shared/tf/tf_weapon_jar.h +++ b/src/game/shared/tf/tf_weapon_jar.h @@ -114,12 +114,15 @@ class CTFCleaver : public CTFJar virtual int GetWeaponID( void ) const { return TF_WEAPON_CLEAVER; } virtual float GetProjectileSpeed( void ); + virtual void PrimaryAttack(void); virtual void SecondaryAttack( void ); virtual const char* GetEffectLabelText( void ) { return "#TF_CLEAVER"; } virtual float InternalGetEffectBarRechargeTime( void ) { return 5.1; } + virtual bool CanCreateCleaver(CTFPlayer* pPlayer); + #ifdef GAME_DLL virtual const AngularImpulse GetAngularImpulse( void ){ return AngularImpulse( 0, 500, 0 ); } virtual Vector GetVelocityVector( const Vector &vecForward, const Vector &vecRight, const Vector &vecUp ); @@ -244,6 +247,7 @@ class CTFProjectile_Cleaver : public CTFProjectile_Jar virtual void OnHit( CBaseEntity *pOther ) OVERRIDE; virtual void Explode( trace_t *pTrace, int bitsDamageType ) OVERRIDE; virtual void Detonate() OVERRIDE; + virtual void DetonateThink() OVERRIDE; virtual const char* GetImpactEffect() OVERRIDE { return ""; } virtual ETFCond GetEffectCondition( void ) OVERRIDE { return TF_COND_BLEEDING; } diff --git a/src/game/shared/tf/tf_weapon_mechanical_arm.cpp b/src/game/shared/tf/tf_weapon_mechanical_arm.cpp index b64d5574e66..b7d3674dff2 100644 --- a/src/game/shared/tf/tf_weapon_mechanical_arm.cpp +++ b/src/game/shared/tf/tf_weapon_mechanical_arm.cpp @@ -250,12 +250,13 @@ void CTFMechanicalArm::SecondaryAttack( void ) if ( ShockAttack() ) { + CalcIsAttackCritical(); WeaponSound( SPECIAL3 ); #ifdef GAME_DLL Vector vecForward, vecRight, vecUp; AngleVectors( pOwner->EyeAngles(), &vecForward, &vecRight, &vecUp ); - float fRight = 8.f; + float fRight = 15.f; if ( IsViewModelFlipped() ) { fRight *= -1; @@ -264,7 +265,7 @@ void CTFMechanicalArm::SecondaryAttack( void ) // vecSrc = vecSrc + ( vecUp * -9.0f ) + ( vecRight * 7.0f ) + ( vecForward * 3.0f ); Vector vecSrc = pOwner->EyePosition() + ( vecForward * 40.f ) - + ( vecRight * 15.f ) + + ( vecRight * fRight ) + ( vecUp * -10.f ); QAngle angForward = pOwner->EyeAngles(); @@ -287,7 +288,8 @@ void CTFMechanicalArm::SecondaryAttack( void ) pOrb->SetAbsVelocity( vForward * tf_mecharm_orb_speed ); pOrb->ChangeTeam( pOwner->GetTeamNumber() ); - pOrb->SetCritical( false ); + pOrb->SetCritical( IsCurrentAttackACrit() ); +// pOrb->SetCritical( false ); DispatchSpawn( pOrb ); } @@ -479,6 +481,8 @@ void CTFMechanicalArm::PrimaryAttack() //int nAmmoToTake = bShocked ? 0 : GetAmmoPerShot(); //pOwner->RemoveAmmo( nAmmoToTake, m_iPrimaryAmmoType ); + CalcIsAttackCritical(); // fix for weapon being incapable of dealing critical hits with primary fire + FireProjectile( pPlayer ); #endif @@ -643,6 +647,9 @@ bool CTFProjectile_MechanicalArmOrb::ShouldProjectileIgnore( CBaseEntity *pOther if ( pOther->IsFuncLOD() ) return true; + if ( pOther->GetFlags() & FL_WORLDBRUSH ) + return true; + const trace_t *pTrace = &CBaseEntity::GetTouchTrace(); if ( pTrace->surface.flags & CONTENTS_LADDER ) return true; @@ -745,7 +752,14 @@ void CTFProjectile_MechanicalArmOrb::CheckForPlayers( int nNumToZap, bool bCanHi info.SetDamage( tf_mecharm_orb_zap_damage ); info.SetDamageCustom( TF_DMG_CUSTOM_PLASMA ); info.SetDamagePosition( GetAbsOrigin() ); - info.SetDamageType( DMG_SHOCK ); + + // the short circuit already can't random crit, so this is in place for when the owner is crit boosted via external means. + int iDmgType = DMG_SHOCK; + if ( IsCritical() ) + { + iDmgType |= DMG_CRITICAL; + } + info.SetDamageType( iDmgType ); CBaseEntity *pListOfEntities[5]; int iEntities = UTIL_EntitiesInSphere( pListOfEntities, 5, GetAbsOrigin(), tf_mecharm_orb_size, FL_CLIENT | FL_FAKECLIENT | FL_NPC ); diff --git a/src/game/shared/tf/tf_weapon_throwable.cpp b/src/game/shared/tf/tf_weapon_throwable.cpp index 3e853c40ec7..4ab63e4cac8 100644 --- a/src/game/shared/tf/tf_weapon_throwable.cpp +++ b/src/game/shared/tf/tf_weapon_throwable.cpp @@ -313,7 +313,7 @@ CTFProjectile_Throwable *CTFThrowable::FireProjectileInternal( void ) Vector vecForward, vecRight, vecUp; AngleVectors( pPlayer->EyeAngles(), &vecForward, &vecRight, &vecUp ); - float fRight = 8.f; + float fRight = 7.f; if ( IsViewModelFlipped() ) { fRight *= -1; @@ -321,7 +321,7 @@ CTFProjectile_Throwable *CTFThrowable::FireProjectileInternal( void ) Vector vecSrc = pPlayer->Weapon_ShootPosition(); // Make spell toss position at the hand - vecSrc = vecSrc + ( vecUp * -9.0f ) + ( vecRight * 7.0f ) + ( vecForward * 3.0f ); + vecSrc = vecSrc + ( vecUp * -9.0f ) + ( vecRight * fRight ) + ( vecForward * 3.0f ); trace_t trace; Vector vecEye = pPlayer->EyePosition(); diff --git a/src/game/shared/tf/tf_weaponbase_gun.cpp b/src/game/shared/tf/tf_weaponbase_gun.cpp index 246b660bb94..ed2a02f41c7 100644 --- a/src/game/shared/tf/tf_weaponbase_gun.cpp +++ b/src/game/shared/tf/tf_weaponbase_gun.cpp @@ -636,6 +636,7 @@ CBaseEntity *CTFWeaponBaseGun::FireNail( CTFPlayer *pPlayer, int iSpecificNail ) // Add some spread float flSpread = 1.5; flSpread += GetProjectileSpread(); + CALL_ATTRIB_HOOK_FLOAT(flSpread, mult_spread_scale); // accuracy multiplier support for server mods CTFBaseProjectile *pProjectile = NULL; switch( iSpecificNail )