//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //============================================================================= #include "cbase.h" #include "tf_weapon_grenadelauncher.h" #include "tf_fx_shared.h" #include "tf_weapon_grenade_pipebomb.h" #include "tf_gamerules.h" #include "in_buttons.h" #include "tf_weaponbase_gun.h" // Client specific. #ifdef CLIENT_DLL #include "c_tf_player.h" #include "c_tf_gamestats.h" #include "bone_setup.h" // Server specific. #else #include "tf_player.h" #include "tf_gamestats.h" #include "tf_fx.h" #endif ConVar tf_double_donk_window( "tf_double_donk_window", "0.5", FCVAR_CHEAT | FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY, "How long after an impact from a cannonball that an explosion will count as a double-donk." ); #define TF_TUBE_COUNT 6 // X is time as a fraction of cProceduralBarrelRotationTime, which is in seconds. // Y is rotation in degrees // Z is slope at Y. // These are hermite spline control points that match maya. const Vector cProceduralBarrelRotationAnimationPoints[] = { Vector( 0, 0, 0 ), Vector( 0.7519f, 63.546f, 0 ), Vector( 1.0f, 60, 0 ) }; static_assert( ARRAYSIZE( cProceduralBarrelRotationAnimationPoints ) > 1, "cProceduralBarrelRotationAnimationPoints must have at least two elements." ); const float cProceduralBarrelRotationTime = 0.2666f; //============================================================================= // // Weapon Grenade Launcher tables. // IMPLEMENT_NETWORKCLASS_ALIASED( TFGrenadeLauncher, DT_WeaponGrenadeLauncher ) BEGIN_NETWORK_TABLE( CTFGrenadeLauncher, DT_WeaponGrenadeLauncher ) #ifdef CLIENT_DLL RecvPropFloat( RECVINFO( m_flDetonateTime ) ), RecvPropInt( RECVINFO( m_iCurrentTube ) ), RecvPropInt( RECVINFO( m_iGoalTube ) ), #else SendPropFloat( SENDINFO( m_flDetonateTime ) ), SendPropInt( SENDINFO( m_iCurrentTube ) ), SendPropInt( SENDINFO( m_iGoalTube ) ), #endif END_NETWORK_TABLE() #ifdef CLIENT_DLL BEGIN_PREDICTION_DATA( CTFGrenadeLauncher ) DEFINE_FIELD( m_flDetonateTime, FIELD_FLOAT ), DEFINE_FIELD( m_iCurrentTube, FIELD_INTEGER ), DEFINE_FIELD( m_iGoalTube, FIELD_INTEGER ) END_PREDICTION_DATA() #endif LINK_ENTITY_TO_CLASS( tf_weapon_grenadelauncher, CTFGrenadeLauncher ); PRECACHE_WEAPON_REGISTER( tf_weapon_grenadelauncher ); CREATE_SIMPLE_WEAPON_TABLE( TFCannon, tf_weapon_cannon ) // Server specific. #ifndef CLIENT_DLL BEGIN_DATADESC( CTFGrenadeLauncher ) END_DATADESC() #endif #define TF_GRENADE_LAUNCER_MIN_VEL 1200 #define TF_DETONATE_MODE_AIR 2 #define TF_WEAPON_CANNON_CHARGE_SOUND "Weapon_LooseCannon.Charge" //============================================================================= // // Weapon Grenade Launcher functions. // //----------------------------------------------------------------------------- // Purpose: // Input : - //----------------------------------------------------------------------------- CTFGrenadeLauncher::CTFGrenadeLauncher() { m_bReloadsSingly = true; #ifdef CLIENT_DLL m_pCannonFuseSparkEffect = NULL; m_pCannonCharge = NULL; #endif // CLIENT_DLL } //----------------------------------------------------------------------------- // Purpose: // Input : - //----------------------------------------------------------------------------- CTFGrenadeLauncher::~CTFGrenadeLauncher() { } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadeLauncher::Spawn( void ) { m_iAltFireHint = HINT_ALTFIRE_GRENADELAUNCHER; BaseClass::Spawn(); ResetDetonateTime(); } //----------------------------------------------------------------------------- // Purpose: Reset the charge when we holster //----------------------------------------------------------------------------- bool CTFGrenadeLauncher::Holster( CBaseCombatWeapon *pSwitchingTo ) { ResetDetonateTime(); return BaseClass::Holster( pSwitchingTo ); } //----------------------------------------------------------------------------- // Purpose: Reset the charge when we deploy //----------------------------------------------------------------------------- bool CTFGrenadeLauncher::Deploy( void ) { ResetDetonateTime(); return BaseClass::Deploy(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CTFGrenadeLauncher::GetMaxClip1( void ) const { #ifdef _X360 return TF_GRENADE_LAUNCHER_XBOX_CLIP; #endif return BaseClass::GetMaxClip1(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CTFGrenadeLauncher::GetDefaultClip1( void ) const { #ifdef _X360 return TF_GRENADE_LAUNCHER_XBOX_CLIP; #endif return BaseClass::GetDefaultClip1(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadeLauncher::PrimaryAttack( void ) { // Check for ammunition. if ( m_iClip1 <= 0 && m_iClip1 != -1 ) return; // Are we capable of firing again? if ( m_flNextPrimaryAttack > gpGlobals->curtime ) return; if ( !CanAttack() ) { ResetDetonateTime(); return; } m_iWeaponMode = TF_WEAPON_PRIMARY_MODE; if ( CanCharge() ) { if ( m_flDetonateTime == 0.f ) { m_flDetonateTime = gpGlobals->curtime + GetMortarDetonateTimeLength(); SendWeaponAnim( ACT_VM_PULLBACK ); #ifdef CLIENT_DLL EmitSound( TF_WEAPON_CANNON_CHARGE_SOUND ); #endif // CLIENT_DLL } else { #ifdef CLIENT_DLL StartChargeEffects(); #endif // CLIENT_DLL } } else { LaunchGrenade(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadeLauncher::ItemPostFrame( void ) { BaseClass::ItemPostFrame(); if ( m_flDetonateTime > 0.f ) { if ( m_flDetonateTime > gpGlobals->curtime ) { CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( !pPlayer ) return; // If we're not holding down the attack button, launch our grenade if ( m_iClip1 > 0 && !(pPlayer->m_nButtons & IN_ATTACK) ) { LaunchGrenade(); } } else { Misfire(); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadeLauncher::Misfire( void ) { BaseClass::Misfire(); LaunchGrenade(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadeLauncher::WeaponIdle( void ) { BaseClass::WeaponIdle(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadeLauncher::FireProjectileInternal( CTFPlayer* pTFPlayer ) { #ifdef GAME_DLL CTFGrenadePipebombProjectile *pProjectile = static_cast( FireProjectile( pTFPlayer ) ); if ( pProjectile ) { if ( GetDetonateMode() == TF_DETONATE_MODE_AIR ) { pProjectile->m_bWallShatter = true; } if ( m_flDetonateTime > 0.f ) { float flDetonateTimeLength = ( gpGlobals->curtime - GetChargeBeginTime() ); pProjectile->SetDetonateTimerLength( flDetonateTimeLength ); if ( flDetonateTimeLength == 0.f ) { trace_t tr; UTIL_TraceLine( pProjectile->GetAbsOrigin(), pTFPlayer->EyePosition(), MASK_SOLID, pProjectile, COLLISION_GROUP_NONE, &tr ); pProjectile->Explode( &tr, GetDamageType() ); } } float flDetonationPenalty = 1.0f; CALL_ATTRIB_HOOK_FLOAT( flDetonationPenalty, grenade_detonation_damage_penalty ); if ( flDetonationPenalty != 1.0f ) { // Setting the initial damage of a grenade lower will set its fused time damage lower // on contact detonations reset the damage to max pProjectile->SetDamage( pProjectile->GetDamage() * flDetonationPenalty ); } } #else FireProjectile( pTFPlayer ); #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadeLauncher::WeaponReset( void ) { BaseClass::WeaponReset(); ResetDetonateTime(); m_iCurrentTube = 0; m_iGoalTube = 0; m_bCurrentAndGoalTubeEqual = true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFGrenadeLauncher::SendWeaponAnim( int iActivity ) { // Client procedurally animates the barrel bone if ( iActivity == ACT_VM_PRIMARYATTACK ) { m_iGoalTube = ( m_iCurrentTube + 1 ) % TF_TUBE_COUNT; m_flBarrelRotateBeginTime = gpGlobals->curtime; } // When we start firing, play the startup firing anim first if ( iActivity == ACT_VM_PRIMARYATTACK ) { // If we're already playing the fire anim, let it continue. It loops. if ( GetActivity() == ACT_VM_PRIMARYATTACK ) return true; // Otherwise, play the start it return BaseClass::SendWeaponAnim( ACT_VM_PRIMARYATTACK ); } return BaseClass::SendWeaponAnim( iActivity ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadeLauncher::PostFire() { // Set next attack times. float flFireDelay = ApplyFireDelay( m_pWeaponInfo->GetWeaponData( m_iWeaponMode ).m_flTimeFireDelay ); m_flNextPrimaryAttack = gpGlobals->curtime + flFireDelay; SetWeaponIdleTime( gpGlobals->curtime + SequenceDuration() ); // Check the reload mode and behave appropriately. if ( m_bReloadsSingly ) { m_iReloadMode.Set( TF_RELOAD_START ); } #ifndef CLIENT_DLL if ( CanCharge() ) { Vector vPosition; QAngle qAngles; if ( GetAttachment( "muzzle", vPosition, qAngles ) ) { CPVSFilter filter( vPosition ); TE_TFParticleEffect( filter, 0.f, "loose_cannon_bang", PATTACH_POINT, this, "muzzle" ); } } #endif ResetDetonateTime(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadeLauncher::LaunchGrenade( void ) { // Get the player owning the weapon. CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( !pPlayer ) return; CalcIsAttackCritical(); SendWeaponAnim( ACT_VM_PRIMARYATTACK ); pPlayer->SetAnimation( PLAYER_ATTACK1 ); pPlayer->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRIMARY ); if ( !AutoFiresFullClipAllAtOnce() ) { FireProjectileInternal( pPlayer ); } else { int nCurrentClipSize = m_iClip1; m_nLauncherSlot = 0; int iSeed = CBaseEntity::GetPredictionRandomSeed() & 255; QAngle punchAngle = pPlayer->GetPunchAngle(); for ( int i=0; iGetPunchAngle(); } } pPlayer->SetPunchAngle( punchAngle ); } #ifdef CLIENT_DLL C_CTF_GameStats.Event_PlayerFiredWeapon( pPlayer, IsCurrentAttackACrit() ); StopSound( TF_WEAPON_CANNON_CHARGE_SOUND ); #else pPlayer->SpeakWeaponFire(); CTF_GameStats.Event_PlayerFiredWeapon( pPlayer, IsCurrentAttackACrit() ); #endif PostFire(); if ( TFGameRules()->GameModeUsesUpgrades() ) { PlayUpgradedShootSound( "Weapon_Upgrade.DamageBonus" ); } } void CTFGrenadeLauncher::AddDonkVictim( const CBaseEntity* pVictim ) { // Clear out old donk victims FOR_EACH_VEC_BACK( m_vecDonkVictims, i ) { if( m_vecDonkVictims[i].m_flExpireTime <= gpGlobals->curtime ) { m_vecDonkVictims.Remove( i ); } } // Add new donk victim Donks_t& donk = m_vecDonkVictims[ m_vecDonkVictims.AddToTail() ]; donk.m_hVictim.Set( pVictim ); donk.m_flExpireTime = gpGlobals->curtime + tf_double_donk_window.GetFloat(); } bool CTFGrenadeLauncher::IsDoubleDonk( const CBaseEntity* pVictim ) const { if( GetWeaponID() != TF_WEAPON_CANNON ) return false; // Check each donk victim to see if we've donked them recently enough to // score a "double-donk" FOR_EACH_VEC( m_vecDonkVictims, i ) { if( gpGlobals->curtime < m_vecDonkVictims[i].m_flExpireTime && m_vecDonkVictims[i].m_hVictim.Get() == pVictim ) { return true; } } return false; } float CTFGrenadeLauncher::GetProjectileSpeed( void ) { CTFPlayer *pOwner = ToTFPlayer( GetOwner() ); if ( pOwner && pOwner->m_Shared.GetCarryingRuneType() == RUNE_PRECISION ) return 3000.f; float flLaunchSpeed = TF_GRENADE_LAUNCER_MIN_VEL; CALL_ATTRIB_HOOK_FLOAT( flLaunchSpeed, mult_projectile_speed ); return flLaunchSpeed; } int CTFGrenadeLauncher::GetDetonateMode( void ) const { int iMode = 0; CALL_ATTRIB_HOOK_INT( iMode, set_detonate_mode ); return iMode; } //----------------------------------------------------------------------------- // Purpose: Detonate this demoman's pipebombs //----------------------------------------------------------------------------- void CTFGrenadeLauncher::SecondaryAttack( void ) { #ifdef GAME_DLL if ( !CanAttack() ) return; CTFPlayer *pOwner = ToTFPlayer( GetOwner() ); pOwner->DoClassSpecialSkill(); #endif } bool CTFGrenadeLauncher::Reload( void ) { return BaseClass::Reload(); } void CTFGrenadeLauncher::FireFullClipAtOnce( void ) { m_iWeaponMode = TF_WEAPON_PRIMARY_MODE; LaunchGrenade(); } bool CTFGrenadeLauncher::CanCharge( void ) { if ( GetWeaponID() == TF_WEAPON_CANNON ) { return GetMortarDetonateTimeLength() > 0.f; } return false; } float CTFGrenadeLauncher::GetChargeBeginTime( void ) { // Inverse begin time logic to get charge bar to decrease from a full bar instead of increase from an empty bar float flMortarDetonateTimeLength = GetMortarDetonateTimeLength(); float flModDetonateTimeLength = flMortarDetonateTimeLength; if ( m_flDetonateTime > 0.f ) { flModDetonateTimeLength = Clamp( m_flDetonateTime - gpGlobals->curtime, 0.f, flMortarDetonateTimeLength ); } return gpGlobals->curtime - flModDetonateTimeLength; } float CTFGrenadeLauncher::GetChargeMaxTime( void ) { return GetMortarDetonateTimeLength(); } void CTFGrenadeLauncher::ResetDetonateTime() { m_flDetonateTime = 0.f; #ifdef CLIENT_DLL StopChargeEffects(); #endif // CLIENT_DLL } float CTFGrenadeLauncher::GetMortarDetonateTimeLength() { float flMortarDetonateTimeLength = 0.f; CALL_ATTRIB_HOOK_FLOAT( flMortarDetonateTimeLength, grenade_launcher_mortar_mode ); return flMortarDetonateTimeLength; } #ifdef CLIENT_DLL void CTFGrenadeLauncher::StartChargeEffects() { if ( !m_pCannonFuseSparkEffect ) { m_pCannonFuseSparkEffect = ParticleProp()->Create( "loose_cannon_sparks", PATTACH_POINT_FOLLOW, "cannon_fuse" ); } if ( !m_pCannonCharge ) { m_pCannonCharge = ParticleProp()->Create( "loose_cannon_buildup_smoke3", PATTACH_POINT_FOLLOW, "muzzle" ); } } void CTFGrenadeLauncher::StopChargeEffects() { if ( m_pCannonFuseSparkEffect ) { ParticleProp()->StopEmission( m_pCannonFuseSparkEffect ); m_pCannonFuseSparkEffect = NULL; } if ( m_pCannonCharge ) { ParticleProp()->StopEmission( m_pCannonCharge ); m_pCannonCharge = NULL; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CStudioHdr *CTFGrenadeLauncher::OnNewModel( void ) { CStudioHdr *hdr = BaseClass::OnNewModel(); m_iBarrelBone = LookupBone( "procedural_chamber" ); return hdr; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadeLauncher::StandardBlendingRules( CStudioHdr *hdr, Vector pos[], Quaternion q[], float currentTime, int boneMask ) { BaseClass::StandardBlendingRules( hdr, pos, q, currentTime, boneMask ); if (m_iBarrelBone != -1) { UpdateBarrelMovement(); AngleQuaternion( RadianEuler( 0, 0, m_flBarrelAngle ), q[m_iBarrelBone] ); } } //----------------------------------------------------------------------------- // Purpose: For third person weapons. //----------------------------------------------------------------------------- void CTFGrenadeLauncher::OnDataChanged( DataUpdateType_t type ) { if ( m_bCurrentAndGoalTubeEqual && m_iCurrentTube != m_iGoalTube ) m_flBarrelRotateBeginTime = gpGlobals->curtime; m_bCurrentAndGoalTubeEqual = ( m_iCurrentTube == m_iGoalTube ); BaseClass::OnDataChanged( type ); } //----------------------------------------------------------------------------- // Purpose: Updates the velocity and position of the rotating barrel //----------------------------------------------------------------------------- void CTFGrenadeLauncher::UpdateBarrelMovement( void ) { if ( m_iGoalTube != m_iCurrentTube ) { float flPartialRotationDeg = 0.0f; const float tVal = ( gpGlobals->curtime - m_flBarrelRotateBeginTime ) / cProceduralBarrelRotationTime; if ( tVal < 1.0f ) { Assert( cProceduralBarrelRotationAnimationPoints[ 0 ].x == 0.0f ); Assert( cProceduralBarrelRotationAnimationPoints[ ARRAYSIZE( cProceduralBarrelRotationAnimationPoints ) - 1 ].x == 1.0f ); const Vector* pFirst = NULL; const Vector* pSecond = NULL; for ( int i = 1; i < ARRAYSIZE( cProceduralBarrelRotationAnimationPoints ); ++i ) { // Need to be increasing in time, or we won't find the right span. Assert( cProceduralBarrelRotationAnimationPoints[ i - 1 ].x < cProceduralBarrelRotationAnimationPoints[ i ].x ); if ( tVal <= cProceduralBarrelRotationAnimationPoints[ i ].x ) { pFirst = &cProceduralBarrelRotationAnimationPoints[ i - 1 ]; pSecond = &cProceduralBarrelRotationAnimationPoints[ i ]; break; } } Assert( pFirst && pSecond ); float flPartialT = ( tVal - pFirst->x ) / ( pSecond->x - pFirst->x ); flPartialRotationDeg = Hermite_Spline( pFirst->y, pSecond->y, pFirst->z, pSecond->z, flPartialT ); } else { m_iCurrentTube = m_iGoalTube; m_bCurrentAndGoalTubeEqual = true; } const float flBaseDeg = 60.0f * m_iCurrentTube; m_flBarrelAngle = DEG2RAD( flBaseDeg + flPartialRotationDeg ); } } void CTFGrenadeLauncher::ViewModelAttachmentBlending( CStudioHdr *hdr, Vector pos[], Quaternion q[], float currentTime, int boneMask ) { int iBarrelBone = Studio_BoneIndexByName( hdr, "procedural_chamber" ); // Assert( iBarrelBone != -1 ); if ( iBarrelBone != -1 ) { if ( hdr->boneFlags( iBarrelBone ) & boneMask ) { RadianEuler a; QuaternionAngles( q[ iBarrelBone ], a ); a.z = m_flBarrelAngle; AngleQuaternion( a, q[ iBarrelBone ] ); } } } #endif //CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: // won't be called for w_ version of the model, so this isn't getting updated twice //----------------------------------------------------------------------------- void CTFGrenadeLauncher::ItemPreFrame( void ) { #ifdef CLIENT_DLL UpdateBarrelMovement(); #endif #ifdef GAME_DLL if ( gpGlobals->curtime > m_flBarrelRotateBeginTime + cProceduralBarrelRotationTime ) m_iCurrentTube = m_iGoalTube; #endif BaseClass::ItemPreFrame(); }