//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Weapon Knife. // //============================================================================= #include "cbase.h" #include "tf_gamerules.h" #include "tf_weapon_knife.h" #include "decals.h" #include "debugoverlay_shared.h" // Client specific. #ifdef CLIENT_DLL #include "c_tf_player.h" #include "c_tf_gamestats.h" // Server specific. #else #include "tf_player.h" #include "tf_gamestats.h" #include "ilagcompensationmanager.h" #endif //============================================================================= // // Weapon Knife tables. // IMPLEMENT_NETWORKCLASS_ALIASED( TFKnife, DT_TFWeaponKnife ) BEGIN_NETWORK_TABLE( CTFKnife, DT_TFWeaponKnife ) #if defined( CLIENT_DLL ) RecvPropBool( RECVINFO( m_bReadyToBackstab ) ), RecvPropBool( RECVINFO( m_bKnifeExists ) ), RecvPropFloat( RECVINFO( m_flKnifeRegenerateDuration ) ), RecvPropFloat( RECVINFO( m_flKnifeMeltTimestamp ) ), #else SendPropBool( SENDINFO( m_bReadyToBackstab ) ), SendPropBool( SENDINFO( m_bKnifeExists ) ), SendPropFloat( SENDINFO( m_flKnifeRegenerateDuration ), 0, SPROP_NOSCALE ), SendPropFloat( SENDINFO( m_flKnifeMeltTimestamp ), 0, SPROP_NOSCALE ), #endif END_NETWORK_TABLE() BEGIN_PREDICTION_DATA( CTFKnife ) #ifdef CLIENT_DLL DEFINE_PRED_FIELD( m_bReadyToBackstab, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE ), #endif END_PREDICTION_DATA() LINK_ENTITY_TO_CLASS( tf_weapon_knife, CTFKnife ); PRECACHE_WEAPON_REGISTER( tf_weapon_knife ); //============================================================================= // // Weapon Knife functions. // //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CTFKnife::CTFKnife() { m_bReadyToBackstab = false; m_flBlockedTime = 0.f; m_bAllowHolsterBecauseForced = false; ResetVars(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFKnife::ResetVars( void ) { m_bKnifeExists = true; m_flKnifeRegenerateDuration = 1.0f; m_flKnifeMeltTimestamp = 0.0f; m_bWasTaunting = false; m_bAllowHolsterBecauseForced = false; } //----------------------------------------------------------------------------- // Purpose: We are regenerating (ie: resupply cabinet) //----------------------------------------------------------------------------- void CTFKnife::WeaponRegenerate( void ) { BaseClass::WeaponRegenerate(); ResetVars(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFKnife::WeaponReset( void ) { BaseClass::WeaponReset(); ResetVars(); } //----------------------------------------------------------------------------- bool CTFKnife::DoSwingTrace( trace_t &trace ) { return BaseClass::DoSwingTrace( trace ); } #ifdef GAME_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFKnife::ApplyOnInjuredAttributes( CTFPlayer *pVictim, CTFPlayer *pAttacker, const CTakeDamageInfo &info ) { BaseClass::ApplyOnInjuredAttributes( pVictim, pAttacker, info ); int iMeltsInFire = 0; CALL_ATTRIB_HOOK_INT( iMeltsInFire, melts_in_fire ); if ( iMeltsInFire > 0 && info.GetDamageType() & DMG_BURN ) { if ( m_bKnifeExists ) { // melt it! m_bKnifeExists = false; m_flKnifeRegenerateDuration = iMeltsInFire; m_flKnifeMeltTimestamp = gpGlobals->curtime; CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( pPlayer ) { pPlayer->EmitSound( "Icicle.Melt" ); // force switch to sapper // Set flag to allow holstering during this forced switch (holstering might otherwise have been inhibited by being blocked). This addresses // a corner-case where a Spy stabbing a Razorback-equipped sniper with the Spycicle and then immediately burned doesn't switch away and could backstab immediately // even though their knife should have melted. CBaseCombatWeapon *mySapper = pPlayer->Weapon_GetWeaponByType( TF_WPN_TYPE_BUILDING ); m_bAllowHolsterBecauseForced = true; if ( !mySapper ) { // this should never happen pPlayer->SwitchToNextBestWeapon( this ); } else { pPlayer->Weapon_Switch( mySapper ); } m_bAllowHolsterBecauseForced = false; } } } } //----------------------------------------------------------------------------- bool CTFKnife::DecreaseRegenerationTime( float value, bool bForce ) { // didn't do anything if ( m_bKnifeExists ) return false; float flTime = value * 0.005f * m_flKnifeRegenerateDuration; m_flKnifeMeltTimestamp -= flTime; return true; } #endif //----------------------------------------------------------------------------- // Purpose: Set stealth attack bool //----------------------------------------------------------------------------- void CTFKnife::PrimaryAttack( void ) { CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( !CanAttack() ) return; // Set the weapon usage mode - primary, secondary. m_iWeaponMode = TF_WEAPON_PRIMARY_MODE; m_hBackstabVictim = NULL; int iBackstabVictimHealth = 0; #if !defined (CLIENT_DLL) // Move other players back to history positions based on local player's lag lagcompensation->StartLagCompensation( pPlayer, pPlayer->GetCurrentCommand() ); #endif trace_t trace; if ( DoSwingTrace( trace ) == true ) { // we will hit something with the attack if( trace.m_pEnt && trace.m_pEnt->IsPlayer() ) { CTFPlayer *pTarget = ToTFPlayer( trace.m_pEnt ); if ( pTarget && pTarget->GetTeamNumber() != pPlayer->GetTeamNumber() ) { // Deal extra damage to players when stabbing them from behind if ( CanPerformBackstabAgainstTarget( pTarget ) ) { // store the victim to compare when we do the damage m_hBackstabVictim.Set( pTarget ); iBackstabVictimHealth = Max( m_hBackstabVictim->GetHealth(), 75 ); } } } } #ifndef CLIENT_DLL pPlayer->RemoveInvisibility(); lagcompensation->FinishLagCompensation( pPlayer ); #endif // Swing the weapon. Swing( pPlayer ); Smack(); m_flSmackTime = -1.0f; m_bReadyToBackstab = false; // Hand is down. #if !defined( CLIENT_DLL ) pPlayer->SpeakWeaponFire(); CTF_GameStats.Event_PlayerFiredWeapon( pPlayer, IsCurrentAttackACrit() ); #endif #ifdef CLIENT_DLL C_CTF_GameStats.Event_PlayerFiredWeapon( pPlayer, IsCurrentAttackACrit() ); #endif bool bSuccessfulBackstab = IsBackstab() && !m_hBackstabVictim->IsAlive(); ETFFlagType ignoreTypes[] = { TF_FLAGTYPE_PLAYER_DESTRUCTION }; if ( ShouldDisguiseOnBackstab() && bSuccessfulBackstab && !pPlayer->HasTheFlag( ignoreTypes, ARRAYSIZE( ignoreTypes ) ) ) { // Different rules in MvM when stabbing bots bool bMvM = TFGameRules() && TFGameRules()->IsMannVsMachineMode() && m_hBackstabVictim->IsBot(); if ( bMvM ) { // Remove the disguise first, otherwise this attribute is overpowered pPlayer->RemoveDisguise(); } // We should very quickly disguise as our victim. const float flDelay = bMvM ? 1.5f : 0.2f; SetContextThink( &CTFKnife::DisguiseOnKill, gpGlobals->curtime + flDelay, "DisguiseOnKill" ); } else { pPlayer->RemoveDisguise(); } #ifdef GAME_DLL int iSanguisuge = 0; CALL_ATTRIB_HOOK_INT( iSanguisuge, sanguisuge ); if ( bSuccessfulBackstab && iSanguisuge > 0 ) { // Our health cap is 3x our default maximum health cap. This is so high to make up for // the fact that our default is lowered by equipping the weapon. int iBaseMaxHealth = pPlayer->GetMaxHealth() * 3, iNewHealth = MIN( pPlayer->GetHealth() + iBackstabVictimHealth, iBaseMaxHealth ), iDeltaHealth = iNewHealth - pPlayer->GetHealth(); if ( iDeltaHealth > 0 ) { pPlayer->TakeHealth( iDeltaHealth, DMG_IGNORE_MAXHEALTH ); pPlayer->m_Shared.HealthKitPickupEffects( iDeltaHealth ); } } #endif // GAME_DLL } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFKnife::DisguiseOnKill() { #ifdef GAME_DLL if ( !m_hBackstabVictim.Get() ) return; int nTeam = m_hBackstabVictim->GetTeamNumber(); int nClass = m_hBackstabVictim->GetPlayerClass()->GetClassIndex(); CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( pPlayer ) { pPlayer->m_Shared.Disguise( nTeam, nClass, m_hBackstabVictim.Get(), true ); } #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFKnife::ShouldDisguiseOnBackstab() { int iDisguiseAsVictim = 0; CALL_ATTRIB_HOOK_INT( iDisguiseAsVictim, set_disguise_on_backstab ); if ( iDisguiseAsVictim == 1 ) return true; else return false; } //----------------------------------------------------------------------------- // Purpose: Do backstab damage //----------------------------------------------------------------------------- float CTFKnife::GetMeleeDamage( CBaseEntity *pTarget, int* piDamageType, int* piCustomDamage ) { float flBaseDamage = BaseClass::GetMeleeDamage( pTarget, piDamageType, piCustomDamage ); CTFPlayer *pTFOwner = ToTFPlayer( GetPlayerOwner() ); if ( !pTFOwner ) return false; if ( pTarget->IsPlayer() ) { if ( IsBackstab() ) { CTFPlayer *pTFTarget = ToTFPlayer( pTarget ); // Special rules in modes where player power grows significantly if ( !pTFOwner->IsBot() && pTFTarget && pTFTarget->IsMiniBoss() ) { // MvM: Cap damage against bots and check for a damage upgrade float flBonusDmg = 1.f; CALL_ATTRIB_HOOK_FLOAT( flBonusDmg, mult_dmg ); flBaseDamage = 250.f * flBonusDmg; // Minibosses: Adjust damage when backstabbing based on level of armor piercing // Base amount is 25% of normal damage. Each level adds 25% to a cap of 125%. float flArmorPiercing = 25.f; CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFOwner, flArmorPiercing, armor_piercing ); flBaseDamage *= clamp( flArmorPiercing / 100.0f, 0.25f, 1.25f ); } else // Regular game mode, or the attacker is a bot { // Do twice the target's health so that random modification will still kill him. flBaseDamage = pTarget->GetHealth() * 2; } // Declare a backstab. *piCustomDamage = TF_DMG_CUSTOM_BACKSTAB; } else if (pTFOwner->m_Shared.IsCritBoosted()) { m_bCurrentAttackIsCrit = true; } else { m_bCurrentAttackIsCrit = false; // don't do a crit if we failed the above checks. } } return flBaseDamage; } //----------------------------------------------------------------------------- // Purpose: Are we in a backstab position? //----------------------------------------------------------------------------- bool CTFKnife::CanPerformBackstabAgainstTarget( CTFPlayer *pTarget ) { if ( !pTarget ) return false; // Immune? int iNoBackstab = 0; CALL_ATTRIB_HOOK_INT_ON_OTHER( pTarget, iNoBackstab, cannot_be_backstabbed ); if ( iNoBackstab ) return false; // Behind and facing target's back? if ( IsBehindAndFacingTarget( pTarget ) ) return true; // Is target (bot) disabled via a sapper? if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && pTarget->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) { if ( pTarget->m_Shared.InCond( TF_COND_MVM_BOT_STUN_RADIOWAVE ) ) return true; if ( pTarget->m_Shared.InCond( TF_COND_SAPPED ) && !pTarget->IsMiniBoss() ) return true; } return false; } //----------------------------------------------------------------------------- // Purpose: Determine if we are reasonably facing our target. //----------------------------------------------------------------------------- bool CTFKnife::IsBehindAndFacingTarget( CTFPlayer *pTarget ) { CTFPlayer *pOwner = ToTFPlayer( GetPlayerOwner() ); if ( !pOwner ) return false; // Get a vector from owner origin to target origin Vector vecToTarget; vecToTarget = pTarget->WorldSpaceCenter() - pOwner->WorldSpaceCenter(); vecToTarget.z = 0.0f; vecToTarget.NormalizeInPlace(); // Get owner forward view vector Vector vecOwnerForward; AngleVectors( pOwner->EyeAngles(), &vecOwnerForward, NULL, NULL ); vecOwnerForward.z = 0.0f; vecOwnerForward.NormalizeInPlace(); // Get target forward view vector Vector vecTargetForward; AngleVectors( pTarget->EyeAngles(), &vecTargetForward, NULL, NULL ); vecTargetForward.z = 0.0f; vecTargetForward.NormalizeInPlace(); // Make sure owner is behind, facing and aiming at target's back float flPosVsTargetViewDot = DotProduct( vecToTarget, vecTargetForward ); // Behind? float flPosVsOwnerViewDot = DotProduct( vecToTarget, vecOwnerForward ); // Facing? float flViewAnglesDot = DotProduct( vecTargetForward, vecOwnerForward ); // Facestab? // Debug // NDebugOverlay::HorzArrow( pTarget->WorldSpaceCenter(), pTarget->WorldSpaceCenter() + 50.0f * vecTargetForward, 5.0f, 0, 255, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); // NDebugOverlay::HorzArrow( pOwner->WorldSpaceCenter(), pOwner->WorldSpaceCenter() + 50.0f * vecOwnerForward, 5.0f, 0, 255, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); // DevMsg( "PosDot: %3.2f FacingDot: %3.2f AnglesDot: %3.2f\n", flPosVsTargetViewDot, flPosVsOwnerViewDot, flViewAnglesDot ); return ( flPosVsTargetViewDot > 0.f && flPosVsOwnerViewDot > 0.5 && flViewAnglesDot > -0.3f ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFKnife::CalcIsAttackCriticalHelper( void ) { // Always crit from behind, never from front return IsBackstab(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFKnife::CalcIsAttackCriticalHelperNoCrits( void ) { // Always crit from behind, never from front return IsBackstab(); } //----------------------------------------------------------------------------- // Purpose: Allow melee weapons to send different anim events // Input : - //----------------------------------------------------------------------------- void CTFKnife::SendPlayerAnimEvent( CTFPlayer *pPlayer ) { if ( IsBackstab() ) { pPlayer->DoAnimationEvent( PLAYERANIMEVENT_CUSTOM_GESTURE, ACT_MP_ATTACK_STAND_SECONDARYFIRE ); } else { pPlayer->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRIMARY ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFKnife::CanDeploy( void ) { m_bKnifeExists = ( gpGlobals->curtime - m_flKnifeMeltTimestamp > m_flKnifeRegenerateDuration ); if ( m_bKnifeExists == false ) { // melted icicle has not yet regenerated return false; } return BaseClass::CanDeploy(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFKnife::Deploy( void ) { bool bDeployed = BaseClass::Deploy(); m_bReadyToBackstab = false; m_bKnifeExists = true; return bDeployed; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFKnife::ItemPreFrame( void ) { BaseClass::ItemPreFrame(); ProcessDisguiseImpulse(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFKnife::ItemPostFrame( void ) { CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( pPlayer ) { if ( pPlayer->m_Shared.InCond( TF_COND_TAUNTING ) ) { m_bWasTaunting = true; } else if ( m_bWasTaunting ) { // we were taunting and now we're not m_bWasTaunting = false; // force switch away from knife if we can't deploy it right now if ( !CanDeploy() ) { // force switch to sapper CBaseCombatWeapon *mySapper = pPlayer->Weapon_GetWeaponByType( TF_WPN_TYPE_BUILDING ); if ( !mySapper ) { // this should never happen pPlayer->SwitchToNextBestWeapon( this ); } else { pPlayer->Weapon_Switch( mySapper ); } } } } BackstabVMThink(); BaseClass::ItemPostFrame(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFKnife::ItemBusyFrame( void ) { BaseClass::ItemBusyFrame(); ProcessDisguiseImpulse(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFKnife::ItemHolsterFrame( void ) { BaseClass::ItemHolsterFrame(); ProcessDisguiseImpulse(); } //----------------------------------------------------------------------------- // Purpose: Special case handling of 'Your Eternal Reward' input polling //----------------------------------------------------------------------------- void CTFKnife::ProcessDisguiseImpulse( void ) { CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( !pPlayer ) return; // Only care about this with the right weapon if ( GetKnifeType() != KNIFE_DISGUISE_ONKILL ) return; // If we're not already disguised, ignore if ( !pPlayer->m_Shared.InCond( TF_COND_DISGUISED ) ) return; pPlayer->m_Shared.ProcessDisguiseImpulse( pPlayer ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFKnife::BackstabVMThink( void ) { CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( !pPlayer ) return; if ( pPlayer->GetActiveWeapon() != this ) return; // Don't do this if we are doing something other than idling. // int iIdealActivity = GetIdealActivity(); // if ( (iIdealActivity != ACT_VM_IDLE) && (iIdealActivity != ACT_BACKSTAB_VM_IDLE) ) // return; int iActivity = GetActivity(); if ( (iActivity != ACT_VM_IDLE) && (iActivity != ACT_BACKSTAB_VM_IDLE) && (iActivity != ACT_MELEE_VM_IDLE) && (iActivity != ACT_ITEM1_VM_IDLE) && (iActivity != ACT_ITEM1_BACKSTAB_VM_IDLE) && (iActivity != ACT_ITEM2_VM_IDLE) && (iActivity != ACT_ITEM2_BACKSTAB_VM_IDLE) ) return; // Are we in backstab range and not cloaked? trace_t trace; if ( DoSwingTrace( trace ) == true && CanAttack() ) { // We will hit something if we attack. if( trace.m_pEnt && trace.m_pEnt->IsPlayer() ) { CTFPlayer *pTarget = ToTFPlayer( trace.m_pEnt ); if ( pTarget && pTarget->GetTeamNumber() != pPlayer->GetTeamNumber() ) { if ( CanPerformBackstabAgainstTarget( pTarget ) ) { if ( !m_bReadyToBackstab ) { SendWeaponAnim( ACT_BACKSTAB_VM_UP ); m_bReadyToBackstab = true; } } else if ( m_bReadyToBackstab ) { SendWeaponAnim( ACT_BACKSTAB_VM_DOWN ); m_bReadyToBackstab = false; } } } } else if ( m_bReadyToBackstab ) { SendWeaponAnim( ACT_BACKSTAB_VM_DOWN ); m_bReadyToBackstab = false; } } //----------------------------------------------------------------------------- // Purpose: Play animation appropriate to ball status. //----------------------------------------------------------------------------- bool CTFKnife::SendWeaponAnim( int iActivity ) { CTFPlayer *pPlayer = GetTFPlayerOwner(); if ( !pPlayer ) return BaseClass::SendWeaponAnim( iActivity ); if ( m_bReadyToBackstab ) { switch ( iActivity ) { case ACT_VM_IDLE: case ACT_ITEM1_VM_IDLE: case ACT_ITEM2_VM_IDLE: iActivity = ACT_BACKSTAB_VM_IDLE; break; } } return BaseClass::SendWeaponAnim( iActivity ); } //----------------------------------------------------------------------------- // Purpose: The spy's backstab was blocked by a player item. //----------------------------------------------------------------------------- void CTFKnife::BackstabBlocked( void ) { CTFPlayer *pPlayer = GetTFPlayerOwner(); if ( !pPlayer ) return; // Delay the spy's next attack for a time. pPlayer->SetNextAttack( gpGlobals->curtime + 2.f ); m_flBlockedTime = gpGlobals->curtime; SendWeaponAnim( ACT_MELEE_VM_STUN ); } //----------------------------------------------------------------------------- // Purpose: Spy can't change weapons if knife is hot. //----------------------------------------------------------------------------- bool CTFKnife::CanHolster( void ) const { if ( !m_bAllowHolsterBecauseForced && gpGlobals->curtime - m_flBlockedTime < 2.f ) return false; else return BaseClass::CanHolster(); }