//========= 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; iWorldSpaceCenter() - 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; iGetTeamNumber() == 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 targetTraceVector; if ( DoSwingTraceInternal( trace, bCleave, &targetTraceVector ) ) { if ( bCleave ) { for ( int i=0; iIsAlive() ) { 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); }