You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1139 lines
32 KiB
1139 lines
32 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
//============================================================================= |
|
|
|
#include "cbase.h" |
|
#include "tf_weaponbase_melee.h" |
|
#include "effect_dispatch_data.h" |
|
#include "tf_gamerules.h" |
|
|
|
// Server specific. |
|
#if !defined( CLIENT_DLL ) |
|
#include "tf_player.h" |
|
#include "tf_gamestats.h" |
|
#include "ilagcompensationmanager.h" |
|
#include "tf_passtime_logic.h" |
|
// Client specific. |
|
#else |
|
#include "c_tf_gamestats.h" |
|
#include "c_tf_player.h" |
|
// NVNT haptics system interface |
|
#include "haptics/ihaptics.h" |
|
#endif |
|
|
|
ConVar tf_weapon_criticals_melee( "tf_weapon_criticals_melee", "1", FCVAR_REPLICATED | FCVAR_NOTIFY, "Controls random crits for melee weapons. 0 - Melee weapons do not randomly crit. 1 - Melee weapons can randomly crit only if tf_weapon_criticals is also enabled. 2 - Melee weapons can always randomly crit regardless of the tf_weapon_criticals setting." ); |
|
|
|
//============================================================================= |
|
// |
|
// TFWeaponBase Melee tables. |
|
// |
|
IMPLEMENT_NETWORKCLASS_ALIASED( TFWeaponBaseMelee, DT_TFWeaponBaseMelee ) |
|
|
|
BEGIN_NETWORK_TABLE( CTFWeaponBaseMelee, DT_TFWeaponBaseMelee ) |
|
END_NETWORK_TABLE() |
|
|
|
BEGIN_PREDICTION_DATA( CTFWeaponBaseMelee ) |
|
END_PREDICTION_DATA() |
|
|
|
LINK_ENTITY_TO_CLASS( tf_weaponbase_melee, CTFWeaponBaseMelee ); |
|
|
|
// Server specific. |
|
#if !defined( CLIENT_DLL ) |
|
BEGIN_DATADESC( CTFWeaponBaseMelee ) |
|
DEFINE_THINKFUNC( Smack ) |
|
END_DATADESC() |
|
#endif |
|
|
|
#ifndef CLIENT_DLL |
|
ConVar tf_meleeattackforcescale( "tf_meleeattackforcescale", "80.0", FCVAR_CHEAT | FCVAR_GAMEDLL | FCVAR_DEVELOPMENTONLY ); |
|
#endif |
|
|
|
#ifdef _DEBUG |
|
extern ConVar tf_weapon_criticals_force_random; |
|
#endif // _DEBUG |
|
|
|
//============================================================================= |
|
// |
|
// TFWeaponBase Melee functions. |
|
// |
|
|
|
// ----------------------------------------------------------------------------- |
|
// Purpose: Constructor. |
|
// ----------------------------------------------------------------------------- |
|
CTFWeaponBaseMelee::CTFWeaponBaseMelee() |
|
{ |
|
WeaponReset(); |
|
} |
|
|
|
// ----------------------------------------------------------------------------- |
|
// Purpose: |
|
// ----------------------------------------------------------------------------- |
|
void CTFWeaponBaseMelee::WeaponReset( void ) |
|
{ |
|
BaseClass::WeaponReset(); |
|
|
|
m_iWeaponMode = TF_WEAPON_PRIMARY_MODE; |
|
m_flSmackTime = -1.0f; |
|
m_bConnected = false; |
|
m_bMiniCrit = false; |
|
} |
|
|
|
// ----------------------------------------------------------------------------- |
|
// Purpose: |
|
// ----------------------------------------------------------------------------- |
|
bool CTFWeaponBaseMelee::CanHolster( void ) const |
|
{ |
|
// For fist users, energy buffs come from steak sandviches which lock us into attacking with melee. |
|
CTFPlayer *pPlayer = GetTFPlayerOwner(); |
|
if ( pPlayer && pPlayer->m_Shared.InCond( TF_COND_CANNOT_SWITCH_FROM_MELEE ) ) |
|
return false; |
|
|
|
return BaseClass::CanHolster(); |
|
} |
|
|
|
// ----------------------------------------------------------------------------- |
|
// Purpose: |
|
// ----------------------------------------------------------------------------- |
|
void CTFWeaponBaseMelee::Precache() |
|
{ |
|
BaseClass::Precache(); |
|
|
|
if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
char szMeleeSoundStr[128] = "MVM_"; |
|
const char *shootsound = GetShootSound( MELEE_HIT ); |
|
if ( shootsound && shootsound[0] ) |
|
{ |
|
V_strcat(szMeleeSoundStr, shootsound, sizeof( szMeleeSoundStr )); |
|
CBaseEntity::PrecacheScriptSound( szMeleeSoundStr ); |
|
} |
|
} |
|
CBaseEntity::PrecacheScriptSound("MVM_Weapon_Default.HitFlesh"); |
|
} |
|
|
|
// ----------------------------------------------------------------------------- |
|
// Purpose: |
|
// ----------------------------------------------------------------------------- |
|
void CTFWeaponBaseMelee::Spawn() |
|
{ |
|
Precache(); |
|
|
|
// Get the weapon information. |
|
WEAPON_FILE_INFO_HANDLE hWpnInfo = LookupWeaponInfoSlot( GetClassname() ); |
|
Assert( hWpnInfo != GetInvalidWeaponInfoHandle() ); |
|
CTFWeaponInfo *pWeaponInfo = dynamic_cast< CTFWeaponInfo* >( GetFileWeaponInfoFromHandle( hWpnInfo ) ); |
|
Assert( pWeaponInfo && "Failed to get CTFWeaponInfo in melee weapon spawn" ); |
|
m_pWeaponInfo = pWeaponInfo; |
|
Assert( m_pWeaponInfo ); |
|
|
|
// No ammo. |
|
m_iClip1 = -1; |
|
|
|
BaseClass::Spawn(); |
|
} |
|
|
|
// ----------------------------------------------------------------------------- |
|
// Purpose: |
|
// ----------------------------------------------------------------------------- |
|
bool CTFWeaponBaseMelee::Holster( CBaseCombatWeapon *pSwitchingTo ) |
|
{ |
|
m_flSmackTime = -1.0f; |
|
if ( GetPlayerOwner() ) |
|
{ |
|
GetPlayerOwner()->m_flNextAttack = gpGlobals->curtime + 0.5; |
|
} |
|
|
|
int iSelfMark = 0; |
|
CALL_ATTRIB_HOOK_INT( iSelfMark, self_mark_for_death ); |
|
if ( iSelfMark ) |
|
{ |
|
CTFPlayer *pPlayer = GetTFPlayerOwner(); |
|
if ( pPlayer ) |
|
{ |
|
pPlayer->m_Shared.AddCond( TF_COND_MARKEDFORDEATH_SILENT, iSelfMark ); |
|
} |
|
} |
|
|
|
return BaseClass::Holster( pSwitchingTo ); |
|
} |
|
|
|
int CTFWeaponBaseMelee::GetSwingRange( void ) |
|
{ |
|
CTFPlayer *pOwner = ToTFPlayer( GetOwner() ); |
|
if ( pOwner && pOwner->m_Shared.InCond( TF_COND_SHIELD_CHARGE ) ) |
|
{ |
|
return 128; |
|
} |
|
else |
|
{ |
|
int iIsSword = 0; |
|
CALL_ATTRIB_HOOK_INT( iIsSword, is_a_sword ) |
|
if ( iIsSword ) |
|
{ |
|
return 72; // swords are typically 72 |
|
} |
|
return 48; |
|
} |
|
} |
|
|
|
|
|
// ----------------------------------------------------------------------------- |
|
// Purpose: |
|
// ----------------------------------------------------------------------------- |
|
void CTFWeaponBaseMelee::PrimaryAttack() |
|
{ |
|
// Get the current player. |
|
CTFPlayer *pPlayer = GetTFPlayerOwner(); |
|
if ( !pPlayer ) |
|
return; |
|
|
|
if ( !CanAttack() ) |
|
return; |
|
|
|
// Set the weapon usage mode - primary, secondary. |
|
m_iWeaponMode = TF_WEAPON_PRIMARY_MODE; |
|
m_bConnected = false; |
|
|
|
pPlayer->EndClassSpecialSkill(); |
|
|
|
// Swing the weapon. |
|
Swing( pPlayer ); |
|
|
|
m_bCurrentAttackIsDuringDemoCharge = pPlayer->m_Shared.GetNextMeleeCrit() != MELEE_NOCRIT; |
|
|
|
if ( pPlayer->m_Shared.GetNextMeleeCrit() == MELEE_MINICRIT ) |
|
{ |
|
m_bMiniCrit = true; |
|
} |
|
else |
|
{ |
|
m_bMiniCrit = false; |
|
} |
|
|
|
#ifdef STAGING_ONLY |
|
// Remove Cond if I attack |
|
if ( pPlayer->m_Shared.InCond( TF_COND_NO_COMBAT_SPEED_BOOST ) ) |
|
{ |
|
pPlayer->m_Shared.RemoveCond( TF_COND_NO_COMBAT_SPEED_BOOST ); |
|
} |
|
#endif |
|
|
|
#if !defined( CLIENT_DLL ) |
|
pPlayer->SpeakWeaponFire(); |
|
CTF_GameStats.Event_PlayerFiredWeapon( pPlayer, IsCurrentAttackACrit() ); |
|
|
|
if ( pPlayer->m_Shared.IsStealthed() && ShouldRemoveInvisibilityOnPrimaryAttack() ) |
|
{ |
|
pPlayer->RemoveInvisibility(); |
|
} |
|
#endif |
|
} |
|
|
|
// ----------------------------------------------------------------------------- |
|
// Purpose: |
|
// ----------------------------------------------------------------------------- |
|
void CTFWeaponBaseMelee::SecondaryAttack() |
|
{ |
|
// semi-auto behaviour |
|
if ( m_bInAttack2 ) |
|
return; |
|
|
|
// Get the current player. |
|
CTFPlayer *pPlayer = GetTFPlayerOwner(); |
|
if ( !pPlayer ) |
|
return; |
|
|
|
pPlayer->DoClassSpecialSkill(); |
|
|
|
m_bInAttack2 = true; |
|
|
|
#ifdef STAGING_ONLY |
|
// Remove Cond if I attack |
|
if ( pPlayer->m_Shared.InCond( TF_COND_NO_COMBAT_SPEED_BOOST ) ) |
|
{ |
|
pPlayer->m_Shared.RemoveCond( TF_COND_NO_COMBAT_SPEED_BOOST ); |
|
} |
|
#endif |
|
|
|
m_flNextSecondaryAttack = gpGlobals->curtime + 0.5; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *pPlayer - |
|
//----------------------------------------------------------------------------- |
|
void CTFWeaponBaseMelee::Swing( CTFPlayer *pPlayer ) |
|
{ |
|
CalcIsAttackCritical(); |
|
|
|
#ifdef GAME_DLL |
|
CTF_GameStats.Event_PlayerFiredWeapon( pPlayer, IsCurrentAttackACrit() ); |
|
#endif |
|
#ifdef CLIENT_DLL |
|
C_CTF_GameStats.Event_PlayerFiredWeapon( pPlayer, IsCurrentAttackACrit() ); |
|
#endif |
|
|
|
// Play the melee swing and miss (whoosh) always. |
|
SendPlayerAnimEvent( pPlayer ); |
|
|
|
DoViewModelAnimation(); |
|
|
|
// Set next attack times. |
|
float flFireDelay = ApplyFireDelay( m_pWeaponInfo->GetWeaponData( m_iWeaponMode ).m_flTimeFireDelay ); |
|
|
|
m_flNextPrimaryAttack = gpGlobals->curtime + flFireDelay; |
|
m_flNextSecondaryAttack = gpGlobals->curtime + flFireDelay; |
|
pPlayer->m_Shared.SetNextStealthTime( m_flNextSecondaryAttack ); |
|
|
|
SetWeaponIdleTime( m_flNextPrimaryAttack + m_pWeaponInfo->GetWeaponData( m_iWeaponMode ).m_flTimeIdleEmpty ); |
|
|
|
if ( IsCurrentAttackACrit() ) |
|
{ |
|
WeaponSound( BURST ); |
|
} |
|
else |
|
{ |
|
WeaponSound( MELEE_MISS ); |
|
} |
|
|
|
#ifdef GAME_DLL |
|
// Remember if there are potential targets when we start our swing. |
|
// If there are, the player is exempt from taking "hurt self on miss" damage |
|
// if ALL of these players have died when our swing has finished, and we didn't hit. |
|
// This guards against me performing a "good" swing and being punished by a friend |
|
// killing my target "out from under me". |
|
CUtlVector< CTFPlayer * > enemyVector; |
|
CollectPlayers( &enemyVector, GetEnemyTeam( pPlayer->GetTeamNumber() ), COLLECT_ONLY_LIVING_PLAYERS ); |
|
|
|
m_potentialVictimVector.RemoveAll(); |
|
const float looseSwingRange = 1.2f * GetSwingRange(); |
|
|
|
for( int i=0; i<enemyVector.Count(); ++i ) |
|
{ |
|
Vector toVictim = enemyVector[i]->WorldSpaceCenter() - pPlayer->Weapon_ShootPosition(); |
|
|
|
if ( toVictim.IsLengthLessThan( looseSwingRange ) ) |
|
{ |
|
m_potentialVictimVector.AddToTail( enemyVector[i] ); |
|
} |
|
} |
|
#endif |
|
|
|
m_flSmackTime = gpGlobals->curtime + m_pWeaponInfo->GetWeaponData( m_iWeaponMode ).m_flSmackDelay; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFWeaponBaseMelee::DoViewModelAnimation( void ) |
|
{ |
|
if ( IsCurrentAttackACrit() ) |
|
{ |
|
if ( SendWeaponAnim( ACT_VM_SWINGHARD ) ) |
|
{ |
|
// check that weapon has the activity |
|
return; |
|
} |
|
} |
|
|
|
Activity act = ( m_iWeaponMode == TF_WEAPON_PRIMARY_MODE ) ? ACT_VM_HITCENTER : ACT_VM_SWINGHARD; |
|
|
|
SendWeaponAnim( act ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Allow melee weapons to send different anim events |
|
// Input : - |
|
//----------------------------------------------------------------------------- |
|
void CTFWeaponBaseMelee::SendPlayerAnimEvent( CTFPlayer *pPlayer ) |
|
{ |
|
pPlayer->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRIMARY ); |
|
} |
|
|
|
// ----------------------------------------------------------------------------- |
|
void CTFWeaponBaseMelee::ItemPreFrame( void ) |
|
{ |
|
int iSelfMark = 0; |
|
CALL_ATTRIB_HOOK_INT( iSelfMark, self_mark_for_death ); |
|
if ( iSelfMark ) |
|
{ |
|
CTFPlayer *pPlayer = GetTFPlayerOwner(); |
|
if ( pPlayer ) |
|
{ |
|
pPlayer->m_Shared.AddCond( TF_COND_MARKEDFORDEATH_SILENT, iSelfMark ); |
|
} |
|
} |
|
|
|
return BaseClass::ItemPreFrame(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : - |
|
//----------------------------------------------------------------------------- |
|
void CTFWeaponBaseMelee::ItemPostFrame() |
|
{ |
|
// Check for smack. |
|
if ( m_flSmackTime > 0.0f && gpGlobals->curtime > m_flSmackTime ) |
|
{ |
|
Smack(); |
|
m_flSmackTime = -1.0f; |
|
CTFPlayer *pPlayer = GetTFPlayerOwner(); |
|
if ( pPlayer ) |
|
{ |
|
pPlayer->m_Shared.SetNextMeleeCrit( MELEE_NOCRIT ); |
|
} |
|
} |
|
|
|
BaseClass::ItemPostFrame(); |
|
} |
|
|
|
|
|
bool CTFWeaponBaseMelee::DoSwingTraceInternal( trace_t &trace, bool bCleave, CUtlVector< trace_t >* pTargetTraceVector ) |
|
{ |
|
// Setup a volume for the melee weapon to be swung - approx size, so all melee behave the same. |
|
static Vector vecSwingMinsBase( -18, -18, -18 ); |
|
static Vector vecSwingMaxsBase( 18, 18, 18 ); |
|
|
|
float fBoundsScale = 1.0f; |
|
CALL_ATTRIB_HOOK_FLOAT( fBoundsScale, melee_bounds_multiplier ); |
|
Vector vecSwingMins = vecSwingMinsBase * fBoundsScale; |
|
Vector vecSwingMaxs = vecSwingMaxsBase * fBoundsScale; |
|
|
|
// Get the current player. |
|
CTFPlayer *pPlayer = GetTFPlayerOwner(); |
|
if ( !pPlayer ) |
|
return false; |
|
|
|
// Setup the swing range. |
|
float fSwingRange = GetSwingRange(); |
|
|
|
// Scale the range and bounds by the model scale if they're larger |
|
// Not scaling down the range for smaller models because midgets need all the help they can get |
|
if ( pPlayer->GetModelScale() > 1.0f ) |
|
{ |
|
fSwingRange *= pPlayer->GetModelScale(); |
|
vecSwingMins *= pPlayer->GetModelScale(); |
|
vecSwingMaxs *= pPlayer->GetModelScale(); |
|
} |
|
|
|
CALL_ATTRIB_HOOK_FLOAT( fSwingRange, melee_range_multiplier ); |
|
|
|
Vector vecForward; |
|
AngleVectors( pPlayer->EyeAngles(), &vecForward ); |
|
Vector vecSwingStart = pPlayer->Weapon_ShootPosition(); |
|
Vector vecSwingEnd = vecSwingStart + vecForward * fSwingRange; |
|
|
|
// In MvM, melee hits from the robot team wont hit teammates to ensure mobs of melee bots don't |
|
// swarm so tightly they hit each other and no-one else |
|
bool bDontHitTeammates = pPlayer->GetTeamNumber() == TF_TEAM_PVE_INVADERS && TFGameRules()->IsMannVsMachineMode(); |
|
CTraceFilterIgnoreTeammates ignoreTeammatesFilter( pPlayer, COLLISION_GROUP_NONE, pPlayer->GetTeamNumber() ); |
|
|
|
if ( bCleave ) |
|
{ |
|
Ray_t ray; |
|
ray.Init( vecSwingStart, vecSwingEnd, vecSwingMins, vecSwingMaxs ); |
|
CBaseEntity *pList[256]; |
|
int nTargetCount = UTIL_EntitiesAlongRay( pList, ARRAYSIZE( pList ), ray, FL_CLIENT|FL_OBJECT ); |
|
|
|
int nHitCount = 0; |
|
for ( int i=0; i<nTargetCount; ++i ) |
|
{ |
|
CBaseEntity *pTarget = pList[i]; |
|
if ( pTarget == pPlayer ) |
|
{ |
|
// don't hit yourself |
|
continue; |
|
} |
|
|
|
if ( bDontHitTeammates && pTarget->GetTeamNumber() == pPlayer->GetTeamNumber() ) |
|
{ |
|
// don't hit teammate |
|
continue; |
|
} |
|
|
|
if ( pTargetTraceVector ) |
|
{ |
|
trace_t tr; |
|
UTIL_TraceModel( vecSwingStart, vecSwingEnd, vecSwingMins, vecSwingMaxs, pTarget, COLLISION_GROUP_NONE, &tr ); |
|
pTargetTraceVector->AddToTail(); |
|
pTargetTraceVector->Tail() = tr; |
|
} |
|
nHitCount++; |
|
} |
|
|
|
return nHitCount > 0; |
|
} |
|
else |
|
{ |
|
bool bSapperHit = false; |
|
|
|
// if this weapon can damage sappers, do that trace first |
|
int iDmgSappers = 0; |
|
CALL_ATTRIB_HOOK_INT( iDmgSappers, set_dmg_apply_to_sapper ); |
|
if ( iDmgSappers != 0 ) |
|
{ |
|
CTraceFilterIgnorePlayers ignorePlayersFilter( NULL, COLLISION_GROUP_NONE ); |
|
UTIL_TraceLine( vecSwingStart, vecSwingEnd, MASK_SOLID, &ignorePlayersFilter, &trace ); |
|
if ( trace.fraction >= 1.0 ) |
|
{ |
|
UTIL_TraceHull( vecSwingStart, vecSwingEnd, vecSwingMins, vecSwingMaxs, MASK_SOLID, &ignorePlayersFilter, &trace ); |
|
} |
|
|
|
if ( trace.fraction < 1.0f && |
|
trace.m_pEnt && |
|
trace.m_pEnt->IsBaseObject() && |
|
trace.m_pEnt->GetTeamNumber() == pPlayer->GetTeamNumber() ) |
|
{ |
|
CBaseObject *pObject = static_cast< CBaseObject* >( trace.m_pEnt ); |
|
if ( pObject->HasSapper() ) |
|
{ |
|
bSapperHit = true; |
|
} |
|
} |
|
} |
|
|
|
if ( !bSapperHit ) |
|
{ |
|
// See if we hit anything. |
|
if ( bDontHitTeammates ) |
|
{ |
|
UTIL_TraceLine( vecSwingStart, vecSwingEnd, MASK_SOLID, &ignoreTeammatesFilter, &trace ); |
|
} |
|
else |
|
{ |
|
CTraceFilterIgnoreFriendlyCombatItems filter( pPlayer, COLLISION_GROUP_NONE, pPlayer->GetTeamNumber() ); |
|
UTIL_TraceLine( vecSwingStart, vecSwingEnd, MASK_SOLID, &filter, &trace ); |
|
} |
|
|
|
if ( trace.fraction >= 1.0 ) |
|
{ |
|
if ( bDontHitTeammates ) |
|
{ |
|
UTIL_TraceHull( vecSwingStart, vecSwingEnd, vecSwingMins, vecSwingMaxs, MASK_SOLID, &ignoreTeammatesFilter, &trace ); |
|
} |
|
else |
|
{ |
|
CTraceFilterIgnoreFriendlyCombatItems filter( pPlayer, COLLISION_GROUP_NONE, pPlayer->GetTeamNumber() ); |
|
UTIL_TraceHull( vecSwingStart, vecSwingEnd, vecSwingMins, vecSwingMaxs, MASK_SOLID, &filter, &trace ); |
|
} |
|
|
|
if ( trace.fraction < 1.0 ) |
|
{ |
|
// Calculate the point of intersection of the line (or hull) and the object we hit |
|
// This is and approximation of the "best" intersection |
|
CBaseEntity *pHit = trace.m_pEnt; |
|
if ( !pHit || pHit->IsBSPModel() ) |
|
{ |
|
// Why duck hull min/max? |
|
FindHullIntersection( vecSwingStart, trace, VEC_DUCK_HULL_MIN, VEC_DUCK_HULL_MAX, pPlayer ); |
|
} |
|
|
|
// This is the point on the actual surface (the hull could have hit space) |
|
vecSwingEnd = trace.endpos; |
|
} |
|
} |
|
} |
|
|
|
return ( trace.fraction < 1.0f ); |
|
} |
|
} |
|
|
|
|
|
bool CTFWeaponBaseMelee::DoSwingTrace( trace_t &trace ) |
|
{ |
|
return DoSwingTraceInternal( trace, false, NULL ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : float |
|
//----------------------------------------------------------------------------- |
|
bool CTFWeaponBaseMelee::OnSwingHit( trace_t &trace ) |
|
{ |
|
CTFPlayer *pPlayer = GetTFPlayerOwner(); |
|
|
|
// NVNT if this is the client dll and the owner is the local player |
|
// Notify the haptics system the local player just hit something. |
|
#ifdef CLIENT_DLL |
|
if(pPlayer==C_TFPlayer::GetLocalTFPlayer() && haptics) |
|
haptics->ProcessHapticEvent(2,"Weapons","meleehit"); |
|
#endif |
|
|
|
bool bHitEnemyPlayer = false; |
|
|
|
// Hit sound - immediate. |
|
if( trace.m_pEnt->IsPlayer() ) |
|
{ |
|
CTFPlayer *pTargetPlayer = ToTFPlayer( trace.m_pEnt ); |
|
|
|
bool bPlayMvMHitOnly = false; |
|
// handle hitting a robot |
|
if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
if ( pTargetPlayer && pTargetPlayer->GetTeamNumber() == TF_TEAM_PVE_INVADERS && !pTargetPlayer->IsPlayer() ) |
|
{ |
|
bPlayMvMHitOnly = true; |
|
|
|
CBroadcastRecipientFilter filter; |
|
// CSingleUserRecipientFilter filter( ToBasePlayer( GetOwner() ) ); |
|
// if ( IsPredicted() && CBaseEntity::GetPredictionPlayer() ) |
|
// { |
|
// filter.UsePredictionRules(); |
|
// } |
|
|
|
char szMeleeSoundStr[128] = "MVM_"; |
|
const char *shootsound = GetShootSound( MELEE_HIT ); |
|
if ( shootsound && shootsound[0] ) |
|
{ |
|
V_strcat(szMeleeSoundStr, shootsound, sizeof( szMeleeSoundStr )); |
|
CSoundParameters params; |
|
if ( CBaseEntity::GetParametersForSound( szMeleeSoundStr, params, NULL ) ) |
|
{ |
|
EmitSound( filter, GetOwner()->entindex(), szMeleeSoundStr, NULL ); |
|
} |
|
else |
|
{ |
|
EmitSound( filter, GetOwner()->entindex(), "MVM_Weapon_Default.HitFlesh", NULL ); |
|
} |
|
} |
|
else |
|
{ |
|
EmitSound( filter, GetOwner()->entindex(), "MVM_Weapon_Default.HitFlesh", NULL ); |
|
} |
|
} |
|
} |
|
if(! bPlayMvMHitOnly ) |
|
{ |
|
WeaponSound( MELEE_HIT ); |
|
} |
|
|
|
#if !defined (CLIENT_DLL) |
|
|
|
if ( pTargetPlayer->m_Shared.HasPasstimeBall() && g_pPasstimeLogic ) |
|
{ |
|
// This handles stealing the ball from teammates since there's no damage involved |
|
// TODO find a better place for this |
|
g_pPasstimeLogic->OnBallCarrierMeleeHit( pTargetPlayer, pPlayer ); |
|
} |
|
|
|
if ( pPlayer->GetTeamNumber() != pTargetPlayer->GetTeamNumber() ) |
|
{ |
|
bHitEnemyPlayer = true; |
|
|
|
if ( TFGameRules()->IsIT( pPlayer ) ) |
|
{ |
|
IGameEvent *pEvent = gameeventmanager->CreateEvent( "tagged_player_as_it" ); |
|
if ( pEvent ) |
|
{ |
|
pEvent->SetInt( "player", pPlayer->GetUserID() ); |
|
gameeventmanager->FireEvent( pEvent, true ); |
|
} |
|
|
|
// Tag! You're IT! |
|
TFGameRules()->SetIT( pTargetPlayer ); |
|
|
|
pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_YES ); |
|
|
|
UTIL_ClientPrintAll( HUD_PRINTTALK, "#TF_HALLOWEEN_BOSS_ANNOUNCE_TAG", pPlayer->GetPlayerName(), pTargetPlayer->GetPlayerName() ); |
|
|
|
CSingleUserReliableRecipientFilter filter( pPlayer ); |
|
pPlayer->EmitSound( filter, pPlayer->entindex(), "Player.TaggedOtherIT" ); |
|
} |
|
} |
|
|
|
if ( pTargetPlayer->InSameTeam( pPlayer ) || pTargetPlayer->m_Shared.GetDisguiseTeam() == GetTeamNumber() ) |
|
{ |
|
int iSpeedBuffOnHit = 0; |
|
CALL_ATTRIB_HOOK_INT( iSpeedBuffOnHit, speed_buff_ally ); |
|
if ( iSpeedBuffOnHit > 0 && trace.m_pEnt ) |
|
{ |
|
pTargetPlayer->m_Shared.AddCond( TF_COND_SPEED_BOOST, 2.f ); |
|
pPlayer->m_Shared.AddCond( TF_COND_SPEED_BOOST, 3.6f ); // give the soldier a bit of additional time to allow them to keep up better with faster classes |
|
|
|
EconEntity_OnOwnerKillEaterEvent( this, pPlayer, pTargetPlayer, kKillEaterEvent_TeammatesWhipped ); // Strange |
|
} |
|
|
|
// Give health to teammates on hit |
|
int nGiveHealthOnHit = 0; |
|
CALL_ATTRIB_HOOK_INT( nGiveHealthOnHit, add_give_health_to_teammate_on_hit ); |
|
if ( nGiveHealthOnHit != 0 ) |
|
{ |
|
// Always keep at least 1 health for ourselves |
|
nGiveHealthOnHit = Min( pPlayer->GetHealth() - 1, nGiveHealthOnHit ); |
|
int nHealthGiven = pTargetPlayer->TakeHealth( nGiveHealthOnHit, DMG_GENERIC ); |
|
|
|
if ( nHealthGiven > 0 ) |
|
{ |
|
// Subtract health given from my own |
|
CTakeDamageInfo info( pPlayer, pPlayer, this, nHealthGiven, DMG_GENERIC | DMG_PREVENT_PHYSICS_FORCE ); |
|
pPlayer->TakeDamage( info ); |
|
} |
|
} |
|
} |
|
#endif |
|
} |
|
else |
|
{ |
|
WeaponSound( MELEE_HIT_WORLD ); |
|
} |
|
|
|
DoMeleeDamage( trace.m_pEnt, trace ); |
|
|
|
return bHitEnemyPlayer; |
|
} |
|
|
|
|
|
// ----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Note: Think function to delay the impact decal until the animation is finished |
|
// playing. |
|
// ----------------------------------------------------------------------------- |
|
void CTFWeaponBaseMelee::Smack( void ) |
|
{ |
|
trace_t trace; |
|
|
|
CTFPlayer *pPlayer = GetTFPlayerOwner(); |
|
if ( !pPlayer ) |
|
return; |
|
|
|
#if !defined (CLIENT_DLL) |
|
// Move other players back to history positions based on local player's lag |
|
lagcompensation->StartLagCompensation( pPlayer, pPlayer->GetCurrentCommand() ); |
|
#endif |
|
|
|
bool bHitEnemyPlayer = false; |
|
|
|
int nCleaveAttack = 0; |
|
CALL_ATTRIB_HOOK_INT( nCleaveAttack, melee_cleave_attack ); |
|
bool bCleave = nCleaveAttack > 0; |
|
|
|
// We hit, setup the smack. |
|
CUtlVector<trace_t> targetTraceVector; |
|
if ( DoSwingTraceInternal( trace, bCleave, &targetTraceVector ) ) |
|
{ |
|
if ( bCleave ) |
|
{ |
|
for ( int i=0; i<targetTraceVector.Count(); ++i ) |
|
{ |
|
bHitEnemyPlayer |= OnSwingHit( targetTraceVector[i] ); |
|
} |
|
} |
|
else |
|
{ |
|
bHitEnemyPlayer = OnSwingHit( trace ); |
|
} |
|
} |
|
else |
|
{ |
|
// if ALL of my potential targets have been killed by someone else between the |
|
// time I started my swing and the time my swing would have landed, don't |
|
// punish me for it. |
|
bool bIsCleanMiss = true; |
|
|
|
#ifdef GAME_DLL |
|
for( int i=0; i<m_potentialVictimVector.Count(); ++i ) |
|
{ |
|
if ( m_potentialVictimVector[i] != NULL && m_potentialVictimVector[i]->IsAlive() ) |
|
{ |
|
bIsCleanMiss = false; |
|
break; |
|
} |
|
} |
|
#endif |
|
|
|
if ( bIsCleanMiss ) |
|
{ |
|
int iHitSelf = 0; |
|
CALL_ATTRIB_HOOK_INT( iHitSelf, hit_self_on_miss ); |
|
if ( iHitSelf == 1 ) |
|
{ |
|
DoMeleeDamage( GetTFPlayerOwner(), trace, 0.5f ); |
|
} |
|
} |
|
} |
|
|
|
#if !defined (CLIENT_DLL) |
|
|
|
// ACHIEVEMENT_TF_MEDIC_BONESAW_NOMISSES |
|
if ( GetWeaponID() == TF_WEAPON_BONESAW ) |
|
{ |
|
int iCount = pPlayer->GetPerLifeCounterKV( "medic_bonesaw_hits" ); |
|
|
|
if ( bHitEnemyPlayer ) |
|
{ |
|
if ( ++iCount >= 5 ) |
|
{ |
|
pPlayer->AwardAchievement( ACHIEVEMENT_TF_MEDIC_BONESAW_NOMISSES ); |
|
} |
|
} |
|
else |
|
{ |
|
iCount = 0; |
|
} |
|
|
|
pPlayer->SetPerLifeCounterKV( "medic_bonesaw_hits", iCount ); |
|
} |
|
|
|
lagcompensation->FinishLagCompensation( pPlayer ); |
|
#endif |
|
} |
|
|
|
void CTFWeaponBaseMelee::DoMeleeDamage( CBaseEntity* ent, trace_t& trace ) |
|
{ |
|
DoMeleeDamage( ent, trace, 1.f ); |
|
} |
|
|
|
void CTFWeaponBaseMelee::DoMeleeDamage( CBaseEntity* ent, trace_t& trace, float flDamageMod ) |
|
{ |
|
// Get the current player. |
|
CTFPlayer *pPlayer = GetTFPlayerOwner(); |
|
if ( !pPlayer ) |
|
return; |
|
|
|
Vector vecForward; |
|
AngleVectors( pPlayer->EyeAngles(), &vecForward ); |
|
Vector vecSwingStart = pPlayer->Weapon_ShootPosition(); |
|
Vector vecSwingEnd = vecSwingStart + vecForward * 48; |
|
|
|
#ifndef CLIENT_DLL |
|
// Do Damage. |
|
int iCustomDamage = GetDamageCustom(); |
|
int iDmgType = DMG_MELEE | DMG_NEVERGIB | DMG_CLUB; |
|
|
|
int iCritFromBehind = 0; |
|
CALL_ATTRIB_HOOK_INT( iCritFromBehind, crit_from_behind ); |
|
if ( iCritFromBehind > 0 ) |
|
{ |
|
Vector entForward; |
|
AngleVectors( ent->EyeAngles(), &entForward ); |
|
|
|
Vector toEnt = ent->GetAbsOrigin() - pPlayer->GetAbsOrigin(); |
|
toEnt.NormalizeInPlace(); |
|
|
|
if ( DotProduct( toEnt, entForward ) > 0.7071f ) |
|
{ |
|
iDmgType |= DMG_CRITICAL; |
|
} |
|
} |
|
|
|
float flDamage = GetMeleeDamage( ent, &iDmgType, &iCustomDamage ) * flDamageMod; |
|
|
|
// Base melee damage increased because we disallow random crits in this mode. Without random crits, melee is underpowered |
|
if ( TFGameRules() && TFGameRules()->IsPowerupMode() ) |
|
{ |
|
if ( !IsCurrentAttackACrit() ) // Don't multiply base damage if attack is a crit |
|
{ |
|
if ( pPlayer && pPlayer->m_Shared.GetCarryingRuneType() == RUNE_KNOCKOUT ) |
|
{ |
|
flDamage *= 1.9f; |
|
} |
|
// Strength powerup multiplies damage later and we only want double regular damage. Shields are a source of increased melee damage (charge crit) so they don't need a base boost |
|
else if ( pPlayer && pPlayer->m_Shared.GetCarryingRuneType() != RUNE_STRENGTH && !pPlayer->m_Shared.IsShieldEquipped() ) |
|
{ |
|
flDamage *= 1.3f; |
|
} |
|
} |
|
} |
|
|
|
if ( IsCurrentAttackACrit() ) |
|
{ |
|
// TODO: Not removing the old critical path yet, but the new custom damage is marking criticals as well for melee now. |
|
iDmgType |= DMG_CRITICAL; |
|
} |
|
else if ( m_bMiniCrit ) |
|
{ |
|
iDmgType |= DMG_RADIUS_MAX; // Unused for melee, indicates this should be a minicrit. |
|
} |
|
|
|
CTakeDamageInfo info( pPlayer, pPlayer, this, flDamage, iDmgType, iCustomDamage ); |
|
|
|
if ( fabs( flDamage ) >= 1.0f ) |
|
{ |
|
CalculateMeleeDamageForce( &info, vecForward, vecSwingEnd, 1.0f / flDamage * GetForceScale() ); |
|
} |
|
else |
|
{ |
|
info.SetDamageForce( vec3_origin ); |
|
} |
|
|
|
ent->DispatchTraceAttack( info, vecForward, &trace ); |
|
ApplyMultiDamage(); |
|
|
|
OnEntityHit( ent, &info ); |
|
|
|
bool bTruce = TFGameRules() && TFGameRules()->IsTruceActive() && pPlayer->IsTruceValidForEnt(); |
|
if ( !bTruce ) |
|
{ |
|
int iCritsForceVictimToLaugh = 0; |
|
CALL_ATTRIB_HOOK_INT( iCritsForceVictimToLaugh, crit_forces_victim_to_laugh ); |
|
if ( iCritsForceVictimToLaugh > 0 && ( IsCurrentAttackACrit() || iDmgType & DMG_CRITICAL ) ) |
|
{ |
|
CTFPlayer *pVictimPlayer = ToTFPlayer( ent ); |
|
|
|
if ( pVictimPlayer && pVictimPlayer->CanBeForcedToLaugh() && ( pPlayer->GetTeamNumber() != pVictimPlayer->GetTeamNumber() ) ) |
|
{ |
|
// force victim to laugh! |
|
pVictimPlayer->Taunt( TAUNT_MISC_ITEM, MP_CONCEPT_TAUNT_LAUGH ); |
|
|
|
// strange stat tracking |
|
EconEntity_OnOwnerKillEaterEvent( this, |
|
ToTFPlayer( GetOwner() ), |
|
pVictimPlayer, |
|
kKillEaterEvent_PlayerTickle ); |
|
} |
|
} |
|
|
|
int iTickleEnemiesWieldingSameWeapon = 0; |
|
CALL_ATTRIB_HOOK_INT( iTickleEnemiesWieldingSameWeapon, tickle_enemies_wielding_same_weapon ); |
|
if ( iTickleEnemiesWieldingSameWeapon > 0 ) |
|
{ |
|
CTFPlayer *pVictimPlayer = ToTFPlayer( ent ); |
|
|
|
if ( pVictimPlayer && pVictimPlayer->CanBeForcedToLaugh() && ( pPlayer->GetTeamNumber() != pVictimPlayer->GetTeamNumber() ) ) |
|
{ |
|
CTFWeaponBase *myWeapon = pPlayer->GetActiveTFWeapon(); |
|
CTFWeaponBase *theirWeapon = pVictimPlayer->GetActiveTFWeapon(); |
|
|
|
if ( myWeapon && theirWeapon ) |
|
{ |
|
CEconItemView *myItem = myWeapon->GetAttributeContainer()->GetItem(); |
|
CEconItemView *theirItem = theirWeapon->GetAttributeContainer()->GetItem(); |
|
|
|
if ( myItem && theirItem && myItem->GetItemDefIndex() == theirItem->GetItemDefIndex() ) |
|
{ |
|
// force victim to laugh! |
|
pVictimPlayer->Taunt( TAUNT_MISC_ITEM, MP_CONCEPT_TAUNT_LAUGH ); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
if ( pPlayer->m_Shared.GetCarryingRuneType() == RUNE_KNOCKOUT ) |
|
{ |
|
CTFPlayer *pVictimPlayer = ToTFPlayer( ent ); |
|
|
|
if ( pVictimPlayer && !pVictimPlayer->InSameTeam( pPlayer ) ) |
|
{ |
|
CPASAttenuationFilter filter( pPlayer ); |
|
Vector origin = pPlayer->GetAbsOrigin(); |
|
Vector vecDir = pVictimPlayer->GetAbsOrigin() - origin; |
|
VectorNormalize( vecDir ); |
|
|
|
if ( !pVictimPlayer->m_Shared.InCond( TF_COND_INVULNERABLE_USER_BUFF ) && |
|
!pVictimPlayer->m_Shared.InCond( TF_COND_INVULNERABLE ) ) |
|
{ |
|
if ( pVictimPlayer->m_Shared.IsCarryingRune() ) |
|
{ |
|
pVictimPlayer->DropRune(); |
|
ClientPrint( pVictimPlayer, HUD_PRINTCENTER, "#TF_Powerup_Knocked_Out" ); |
|
} |
|
else if ( pVictimPlayer->HasTheFlag() ) |
|
{ |
|
pVictimPlayer->DropFlag(); |
|
ClientPrint( pVictimPlayer, HUD_PRINTCENTER, "#TF_CTF_PlayerDrop" ); |
|
} |
|
} |
|
EmitSound( filter, entindex(), "Powerup.Knockout_Melee_Hit" ); |
|
pVictimPlayer->ApplyAirBlastImpulse( vecDir * 400.0f ); |
|
} |
|
} |
|
|
|
#endif |
|
// Don't impact trace friendly players or objects |
|
if ( ent && ent->GetTeamNumber() != pPlayer->GetTeamNumber() ) |
|
{ |
|
#ifdef CLIENT_DLL |
|
UTIL_ImpactTrace( &trace, DMG_CLUB ); |
|
#endif |
|
m_bConnected = true; |
|
} |
|
} |
|
|
|
#ifndef CLIENT_DLL |
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : float |
|
//----------------------------------------------------------------------------- |
|
float CTFWeaponBaseMelee::GetForceScale( void ) |
|
{ |
|
return tf_meleeattackforcescale.GetFloat(); |
|
} |
|
#endif |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : float |
|
//----------------------------------------------------------------------------- |
|
float CTFWeaponBaseMelee::GetMeleeDamage( CBaseEntity *pTarget, int* piDamageType, int* piCustomDamage ) |
|
{ |
|
float flDamage = m_pWeaponInfo->GetWeaponData( m_iWeaponMode ).m_nDamage; |
|
CALL_ATTRIB_HOOK_FLOAT( flDamage, mult_dmg ); |
|
|
|
int iCritDoesNoDamage = 0; |
|
CALL_ATTRIB_HOOK_INT( iCritDoesNoDamage, crit_does_no_damage ); |
|
if ( iCritDoesNoDamage > 0 ) |
|
{ |
|
if ( IsCurrentAttackACrit() ) |
|
{ |
|
return 0.0f; |
|
} |
|
|
|
if ( piDamageType && *piDamageType & DMG_CRITICAL ) |
|
{ |
|
return 0.0f; |
|
} |
|
} |
|
|
|
CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); |
|
if ( pPlayer ) |
|
{ |
|
float flHalfHealth = pPlayer->GetMaxHealth() * 0.5f; |
|
if ( pPlayer->GetHealth() < flHalfHealth ) |
|
{ |
|
CALL_ATTRIB_HOOK_FLOAT( flDamage, mult_dmg_bonus_while_half_dead ); |
|
} |
|
else |
|
{ |
|
CALL_ATTRIB_HOOK_FLOAT( flDamage, mult_dmg_penalty_while_half_alive ); |
|
} |
|
|
|
// Some weapons change damage based on player's health |
|
float flReducedHealthBonus = 1.0f; |
|
CALL_ATTRIB_HOOK_FLOAT( flReducedHealthBonus, mult_dmg_with_reduced_health ); |
|
if ( flReducedHealthBonus != 1.0f ) |
|
{ |
|
float flHealthFraction = clamp( pPlayer->HealthFraction(), 0.0f, 1.0f ); |
|
flReducedHealthBonus = Lerp( flHealthFraction, flReducedHealthBonus, 1.0f ); |
|
|
|
flDamage *= flReducedHealthBonus; |
|
} |
|
} |
|
|
|
return flDamage; |
|
} |
|
|
|
void CTFWeaponBaseMelee::OnEntityHit( CBaseEntity *pEntity, CTakeDamageInfo *info ) |
|
{ |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
//----------------------------------------------------------------------------- |
|
bool CTFWeaponBaseMelee::CalcIsAttackCriticalHelperNoCrits( void ) |
|
{ |
|
// This function was called because the tf_weapon_criticals ConVar is off, but if |
|
// melee crits are set to be forced on, then call the regular crit helper function. |
|
if ( tf_weapon_criticals_melee.GetInt() > 1 ) |
|
{ |
|
return CalcIsAttackCriticalHelper(); |
|
} |
|
|
|
CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); |
|
if ( !pPlayer ) |
|
return false; |
|
|
|
m_bCurrentAttackIsDuringDemoCharge = pPlayer->m_Shared.GetNextMeleeCrit() != MELEE_NOCRIT; |
|
|
|
if ( pPlayer->m_Shared.GetNextMeleeCrit() == MELEE_CRIT ) |
|
{ |
|
return true; |
|
} |
|
else |
|
{ |
|
return BaseClass::CalcIsAttackCriticalHelperNoCrits(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CTFWeaponBaseMelee::CalcIsAttackCriticalHelper( void ) |
|
{ |
|
// If melee crits are off, then check the NoCrits helper. |
|
if ( tf_weapon_criticals_melee.GetInt() == 0 ) |
|
{ |
|
return CalcIsAttackCriticalHelperNoCrits(); |
|
} |
|
|
|
CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); |
|
if ( !pPlayer ) |
|
return false; |
|
|
|
if ( !CanFireCriticalShot() ) |
|
return false; |
|
|
|
// Crit boosted players fire all crits |
|
if ( pPlayer->m_Shared.IsCritBoosted() ) |
|
return true; |
|
|
|
float flPlayerCritMult = pPlayer->GetCritMult(); |
|
float flCritChance = TF_DAMAGE_CRIT_CHANCE_MELEE * flPlayerCritMult; |
|
CALL_ATTRIB_HOOK_FLOAT( flCritChance, mult_crit_chance ); |
|
|
|
// mess with the crit chance seed so it's not based solely on the prediction seed |
|
int iMask = ( entindex() << 16 ) | ( pPlayer->entindex() << 8 ); |
|
int iSeed = CBaseEntity::GetPredictionRandomSeed() ^ iMask; |
|
if ( iSeed != m_iCurrentSeed ) |
|
{ |
|
m_iCurrentSeed = iSeed; |
|
RandomSeed( m_iCurrentSeed ); |
|
} |
|
|
|
m_bCurrentAttackIsDuringDemoCharge = pPlayer->m_Shared.GetNextMeleeCrit() != MELEE_NOCRIT; |
|
|
|
if ( pPlayer->m_Shared.GetNextMeleeCrit() == MELEE_CRIT ) |
|
{ |
|
return true; |
|
} |
|
|
|
// Regulate crit frequency to reduce client-side seed hacking |
|
float flDamage = m_pWeaponInfo->GetWeaponData( m_iWeaponMode ).m_nDamage; |
|
CALL_ATTRIB_HOOK_FLOAT( flDamage, mult_dmg ); |
|
AddToCritBucket( flDamage ); |
|
|
|
// Track each request |
|
m_nCritChecks++; |
|
|
|
bool bCrit = ( RandomInt( 0, WEAPON_RANDOM_RANGE-1 ) < ( flCritChance ) * WEAPON_RANDOM_RANGE ); |
|
|
|
#ifdef _DEBUG |
|
// Force seed to always say yes |
|
if ( tf_weapon_criticals_force_random.GetInt() ) |
|
{ |
|
bCrit = true; |
|
} |
|
#endif // _DEBUG |
|
|
|
if ( bCrit ) |
|
{ |
|
// Seed says crit. Run it by the manager. |
|
bCrit = IsAllowedToWithdrawFromCritBucket( flDamage ); |
|
} |
|
|
|
return bCrit; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
char const *CTFWeaponBaseMelee::GetShootSound( int iIndex ) const |
|
{ |
|
// Custom Melee weapons may override their hit effects |
|
if ( iIndex == MELEE_HIT ) |
|
{ |
|
const CEconItemView *pItem = GetAttributeContainer()->GetItem(); |
|
if ( pItem->IsValid() ) |
|
{ |
|
const char *pszSound = pItem->GetStaticData()->GetCustomSound( GetTeamNumber(), 1 ); |
|
if ( pszSound ) |
|
return pszSound; |
|
} |
|
} |
|
|
|
return BaseClass::GetShootSound(iIndex); |
|
}
|
|
|