//========= 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<CTFGrenadePipebombProjectile*>( 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; i<nCurrentClipSize; ++i, ++iSeed )
		{
			RandomSeed( iSeed );
			FireProjectileInternal( pPlayer );
			if ( i == 0 )
			{
				punchAngle = pPlayer->GetPunchAngle();
			}
		}
		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();
}