//========= Copyright © 1996-2002, Valve LLC, All rights reserved. ============
//
// Purpose: Sapper's draining beam
//
// $NoKeywords: $
//=============================================================================
#include "cbase.h"
#include "basetfplayer_shared.h"
#include "in_buttons.h"
#include "weapon_combatshield.h"
#include "engine/ienginesound.h"
#include "grenade_base_empable.h"

#if defined( CLIENT_DLL )

#include "particles_simple.h"

#else

#include "tf_gamerules.h"
#include "tf_class_sapper.h"

#endif

#include "weapon_drainbeam.h"

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"


#define PARTICLE_PATH_VEL				140.0
#define NUM_PATH_PARTICLES_PER_SEC		600.0f
#define NUM_DRIBBLE_PARTICLES_PER_SEC	20.0f

#define NUM_DRAINBEAM_PATH_POINTS		8

char *g_pDrainingSound  = "weapons/repair_gun/healing.wav";
char *g_pNoDrainTargetSound = "weapons/repair_gun/notarget.wav";

// Buff ranges
static ConVar weapon_drainbeam_target_range( "weapon_drainbeam_target_range", "900", FCVAR_REPLICATED, "The farthest away you can be for the drain gun to initially lock onto a target." );
static ConVar weapon_drainbeam_stick_range( "weapon_drainbeam_stick_range", "612", FCVAR_REPLICATED, "How far away the drain gun can stay locked onto a target." );
static ConVar weapon_drainbeam_max_rate( "weapon_drainbeam_max_rate", "10", FCVAR_REPLICATED );
static ConVar weapon_drainbeam_max_rate_time( "weapon_drainbeam_max_rate_time", "5", FCVAR_REPLICATED );
static ConVar weapon_drainbeam_emp_time_factor( "weapon_drainbeam_emp_time_factor", "0.75", FCVAR_REPLICATED );


LINK_ENTITY_TO_CLASS( weapon_drainbeam, CWeaponDrainBeam );

PRECACHE_WEAPON_REGISTER( weapon_drainbeam );

IMPLEMENT_NETWORKCLASS_ALIASED( WeaponDrainBeam, DT_WeaponDrainBeam )

BEGIN_NETWORK_TABLE( CWeaponDrainBeam, DT_WeaponDrainBeam )
#if !defined( CLIENT_DLL )
	SendPropInt( SENDINFO( m_bDraining ), 1, SPROP_UNSIGNED ),
	SendPropInt( SENDINFO( m_bAttacking ), 1, SPROP_UNSIGNED ),
	SendPropVector( SENDINFO( m_vFireTarget ), 0, SPROP_COORD ),
	SendPropEHandle( SENDINFO( m_hDrainTarget ) ),
#else
	RecvPropInt( RECVINFO( m_bAttacking ) ),
	RecvPropInt( RECVINFO( m_bDraining ) ),
	RecvPropVector( RECVINFO(m_vFireTarget) ),
	RecvPropEHandle( RECVINFO(m_hDrainTarget) ),
#endif
END_NETWORK_TABLE()

BEGIN_PREDICTION_DATA( CWeaponDrainBeam  )

	DEFINE_PRED_FIELD( CWeaponDrainBeam, m_bDraining, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE ),
	DEFINE_PRED_FIELD( CWeaponDrainBeam, m_bAttacking, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE ),
	DEFINE_PRED_FIELD_TOL( CWeaponDrainBeam, m_vFireTarget, FIELD_VECTOR, FTYPEDESC_INSENDTABLE, 0.125f ),

	DEFINE_PRED_FIELD( CWeaponDrainBeam, m_hDrainTarget, FIELD_EHANDLE, FTYPEDESC_INSENDTABLE ),

//	DEFINE_PRED_FIELD( CWeaponDrainBeam, m_pEmitter, FIELD_POINTER ),
//	DEFINE_PRED_FIELD( CWeaponDrainBeam, m_hParticleMaterial, FIELD_???,  ),
//	DEFINE_PRED_FIELD( CWeaponDrainBeam, m_PathParticleEvent, FIELD_???,  ),
//	DEFINE_PRED_FIELD( CWeaponDrainBeam, m_DribbleParticleEvent, FIELD_??? ),
//	DEFINE_PRED_FIELD( CWeaponDrainBeam, m_bPlayingSound, FIELD_BOOLEAN ),

END_PREDICTION_DATA()

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
CWeaponDrainBeam::CWeaponDrainBeam( void )
{
	m_bDraining = false;
	m_bAttacking = false;
	m_flNextBuzzTime = 0;

	SetPredictionEligible( true );
#if defined( CLIENT_DLL )
	m_pEmitter = NULL;
	m_hParticleMaterial = INVALID_MATERIAL_HANDLE;
	m_PathParticleEvent.Init( NUM_PATH_PARTICLES_PER_SEC );
	m_DribbleParticleEvent.Init( NUM_DRIBBLE_PARTICLES_PER_SEC ); 
	m_bPlayingSound = false;
#endif
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
bool CWeaponDrainBeam::Holster( CBaseCombatWeapon *pSwitchingTo )
{
	RemoveDrainTarget();
	
	return BaseClass::Holster( pSwitchingTo );
}

//-----------------------------------------------------------------------------
// Purpose: Returns a pointer to a drainable target
//-----------------------------------------------------------------------------
CBaseEntity *CWeaponDrainBeam::GetTargetToDrain( CBaseEntity *pCurDrainTarget )
{
	CBaseTFPlayer *pOwner = ToBaseTFPlayer( GetOwner() );
	if ( !pOwner )
		return NULL;

#if !defined( CLIENT_DLL )
	Vector vecSrc = pOwner->Weapon_ShootPosition( );

	// ROBIN: Disabled drainbeam locking to target
/*
	// If we're already draining something, stick onto it as long as possible.
	CBaseEntity *pEntity = pCurDrainTarget;
	if ( pEntity && pEntity->IsAlive() )
	{
		// Make sure the target didn't go out of range.
		Vector vecTargetCenter = pEntity->Center();
		if ( (vecTargetCenter - vecSrc).Length() < weapon_drainbeam_stick_range.GetFloat() )
		{
			// Now make sure there isn't something other than team players in the way.
			class CDrainFilter : public CTraceFilterSimple
			{
			public:
				CDrainFilter( CBaseEntity *pShooter ) : CTraceFilterSimple( pShooter, TFCOLLISION_GROUP_WEAPON )
				{
					m_pShooter = pShooter;
				}

				virtual bool ShouldHitEntity( IServerEntity *pServerEntity, int contentsMask )
				{
					// If it hit an edict the isn't the target and is on our team, then the ray is blocked.
					CBaseEntity *pEnt = (CBaseEntity *)pServerEntity;

					// Ignore collisions with the shooter
					if ( pEnt == m_pShooter )
						return false;

					// Ignore neutral objects
					if ( pEnt->GetTeamNumber() == 0 )
						return false;

					// Ignore collisions with teammates
					if ( pEnt->GetTeam() == m_pShooter->GetTeam() )
						return false;

					// Ignore players
					if ( pEnt->IsPlayer() )
						return false;

					return CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask );
				}

				CBaseEntity	*m_pShooter;
			};

			trace_t tr;
			CDrainFilter drainFilter( pOwner );
			engine->TraceLine( vecSrc, vecTargetCenter, MASK_SHOT, &drainFilter, &tr );
			
			if (( tr.fraction == 1.0f) || (tr.m_pEnt == pEntity))
				return pEntity;
		}

		// Return null so we can't target this player but m_hDrainTarget stays set to them.
		return NULL;
	}
	else
*/
	{	
		// Ok, try to find a new target to drain.
		// Get the target point and location
		Vector vecAiming;
		pOwner->EyeVectors( &vecAiming );

		// Find a player in range of this player, and make sure they're drainable.
		Vector vecEnd = vecSrc + vecAiming * weapon_drainbeam_target_range.GetFloat();
		trace_t tr;

		// Use WeaponTraceLine so shields are tested...
		TFGameRules()->WeaponTraceLine( vecSrc, vecEnd, MASK_SHOT & (~CONTENTS_HITBOX), pOwner, DMG_PROBE, &tr );
		
		if ( tr.fraction != 1.0 )
		{
			CBaseEntity *pEntity = tr.m_pEnt;
			if ( pEntity && (pEntity != pOwner) && pEntity->IsAlive() )
			{
				// Target needs to not be a player, take EMP damage, and needs to be an enemy
				if ( !pEntity->IsPlayer() && pEntity->CanBePoweredUp() && pEntity->GetTeamNumber() != 0 && !pEntity->InSameTeam( pOwner ) )
				{
					// If we've just locked onto a new target, restart our drain rate
					if ( pCurDrainTarget != pEntity )
					{
						m_flDrainStartedAt = gpGlobals->curtime;
					}
					return pEntity;
				}
			}
		}

		return NULL;
	}
#endif

	return NULL;
}

//-----------------------------------------------------------------------------
// Purpose: Overloaded to handle the hold-down draining
//-----------------------------------------------------------------------------
void CWeaponDrainBeam::ItemPostFrame( void )
{
	CBaseTFPlayer *pOwner = ToBaseTFPlayer( GetOwner() );
	if ( !pOwner )
		return;

	// Try to start draining
	m_bAttacking = false;
	if ( (pOwner->m_nButtons & IN_ATTACK) && GetShieldState() == SS_DOWN )
	{
		PrimaryAttack();
		m_bAttacking = true;
	}
	else if ( GetCurDrainTarget() )
	{
		// Detach from the player if they release the attack button.
		RemoveDrainTarget();
	}

	// Prevent shield post frame if we're not ready to attack, or we're draining
	AllowShieldPostFrame( !m_bAttacking );

	WeaponIdle();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CWeaponDrainBeam::RemoveDrainTarget( void )
{
	m_hDrainTarget = NULL;
	m_bDraining = false;
}

//-----------------------------------------------------------------------------
// Purpose: Attempt to drain any player within range of the medikit
//-----------------------------------------------------------------------------
void CWeaponDrainBeam::PrimaryAttack( void )
{
	CBaseTFPlayer *pOwner = ToBaseTFPlayer( GetOwner() );
	if ( !pOwner )
		return;

#if !defined( CLIENT_DLL )
	// Find a target to drain
	CBaseEntity *pTarget = GetTargetToDrain( GetCurDrainTarget() );
	if ( pTarget )
	{
		// EMP the target
		if ( pTarget->CanBePoweredUp() )
		{
			// Increase the EMP time based upon the time we spent draining
			float flDrainTime = (gpGlobals->curtime - m_flDrainStartedAt);
			float flPostDrainTime = weapon_drainbeam_emp_time_factor.GetFloat() * flDrainTime;
			if ( pTarget->AttemptToPowerup( POWERUP_EMP, flPostDrainTime ) )
			{
				// Crank up the drain rate based on the time since we started draining this target
				flDrainTime = min( weapon_drainbeam_max_rate_time.GetFloat(), flDrainTime );
				float flDrainAmount = weapon_drainbeam_max_rate.GetFloat() * ( flDrainTime / weapon_drainbeam_max_rate_time.GetFloat() );

				// It uses floating point in here so it doesn't lose the fractional part on small frame times.
				static float flCurDamageAmt = 0.99;
				float flOldDamageAmt = flCurDamageAmt;
				flCurDamageAmt += gpGlobals->frametime * flDrainAmount;
				int iDamage = (int)flCurDamageAmt - (int)flOldDamageAmt;

				// Accumulated enough to do something?
				if ( iDamage )
				{
					// EMPed my target, make it take damage
					if ( pTarget->GetHealth() )
					{
						pTarget->TakeDamage( CTakeDamageInfo( this, pOwner, iDamage, DMG_GENERIC ) );
					}

					// Increase my health
					if ( pOwner->GetHealth() < pOwner->GetMaxHealth() )
					{
						int maxHealthToAdd = pOwner->GetMaxHealth() - pOwner->GetHealth();
						int nHealthAdded = min( iDamage, maxHealthToAdd );
						pOwner->TakeHealth( nHealthAdded, DMG_GENERIC );
					}

					// If I'm a sapper, I store off the energy
					if ( pOwner->IsClass( TFCLASS_SAPPER ) )
					{
						((CPlayerClassSapper*)pOwner->GetPlayerClass())->AddDrainedEnergy( iDamage );
					}
				}
			}
		}

		// Tell the client who we're trying to drain.
		m_bDraining = true;
		m_vFireTarget = pTarget->WorldSpaceCenter();
		m_hDrainTarget = pTarget;
	}
	else
	{
		RemoveDrainTarget();
	}
#endif

	// Fire continuously.
	m_flNextPrimaryAttack = gpGlobals->curtime;

	CheckRemoveDisguise();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CWeaponDrainBeam::PlayAttackAnimation( int activity )
{
	SendWeaponAnim( activity );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CWeaponDrainBeam::WeaponIdle( void )
{
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CWeaponDrainBeam::GainedNewTechnology( CBaseTechnology *pTechnology )
{
}

#if defined( CLIENT_DLL )

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CWeaponDrainBeam::StopDrainSound( bool bStopHealingSound, bool bStopNoTargetSound )
{
	if ( bStopHealingSound )
		StopSound( entindex(), CHAN_WEAPON, g_pDrainingSound );

	if ( bStopNoTargetSound )
		StopSound( entindex(), CHAN_WEAPON, g_pNoDrainTargetSound );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CWeaponDrainBeam::OnDataChanged( DataUpdateType_t updateType )
{
	BaseClass::OnDataChanged( updateType );

	if ( updateType == DATA_UPDATE_CREATED )
	{
		m_pEmitter = CSimpleEmitter::Create( "C_WeaponDrainBeam" );
		m_hParticleMaterial = m_pEmitter->GetPMaterial( "sprites/chargeball" );

		ClientThinkList()->SetNextClientThink( GetClientHandle(), CLIENT_THINK_ALWAYS );
	}

	// Think?
	if ( !m_bDraining )
	{
		m_bPlayingSound = false;
		StopDrainSound( true, false );
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CWeaponDrainBeam::ClientThink()
{
	C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
	if ( !pPlayer )
		return;

	// Don't show it while the player is dead. Ideally, we'd respond to m_bDraining in OnDataChanged,
	// but it stops sending the weapon when it's holstered, and it gets holstered when the player dies.
	C_BasePlayer *pFiringPlayer = dynamic_cast< C_BasePlayer* >( GetOwner() );
	if ( !pFiringPlayer || pFiringPlayer->IsPlayerDead() )
	{
		m_bPlayingSound = false;
		StopDrainSound();
		return;
	}

	// Abort if we're not draining, or the player doesn't have the fire button down
	if ( !m_bDraining && !m_bAttacking )
		return;

	// Ready for particle emission, sir
	m_pEmitter->SetSortOrigin( (m_vFireTarget + pPlayer->GetAbsOrigin()) * 0.5f );
	float flCur = gpGlobals->frametime;
	Vector vForward, vOrigin;
	QAngle vAngles;
	GetShootPosition( vOrigin, vAngles );
	AngleVectors( vAngles, &vForward );

	// Are they holding the attack button but not draining anyone? Give feedback.
	if ( !m_bDraining )
	{
		// Add random short-lived particles from the gun tip to the target.
		while ( m_DribbleParticleEvent.NextEvent( flCur ) )
		{
			Vector vPos = vOrigin;
			vPos += RandomVector( -2, 2 );
			SimpleParticle *pParticle = m_pEmitter->AddSimpleParticle( m_hParticleMaterial, vPos );
			if ( pParticle )
			{
				// Move the points along the path.
				pParticle->m_vecVelocity = vForward;
				pParticle->m_vecVelocity *= 50;

				pParticle->m_flRoll = 0;
				pParticle->m_flRollDelta = 0;
				pParticle->m_flDieTime = 0.4f;
				pParticle->m_flLifetime = 0;
				pParticle->m_uchColor[0] = 255; 
				pParticle->m_uchColor[1] = 255;
				pParticle->m_uchColor[2] = 255;
				pParticle->m_uchStartAlpha = 32;
				pParticle->m_uchEndAlpha = 0;
				pParticle->m_uchStartSize = 4;
				pParticle->m_uchEndSize = 0;
				pParticle->m_iFlags = 0;
			}
		}
		return;
	}

	if ( !m_bPlayingSound )
	{
		m_bPlayingSound = true;
		CLocalPlayerFilter filter;
		EmitSound( filter, entindex(), CHAN_WEAPON, g_pDrainingSound, 0.7, ATTN_NORM  );
	}

	Vector points[NUM_DRAINBEAM_PATH_POINTS];

	// First generate a sequence of points so we can parameterize the (curvy) path from the
	// tip of the gun to the target.
	Vector vDirTo = m_vFireTarget - vOrigin;
	float flDistanceTo = vDirTo.Length();
	vDirTo /= flDistanceTo;

	float flBendDist = flDistanceTo * 3;
	const Vector  A = vOrigin - vForward * flBendDist;
	const Vector &B = vOrigin;
	const Vector &C = m_vFireTarget;
	const Vector  D = m_vFireTarget - vForward * flBendDist;
	
	for ( int i=0; i < NUM_DRAINBEAM_PATH_POINTS; i++ )
	{
		Catmull_Rom_Spline( A, B, C, D, (float)i / (NUM_DRAINBEAM_PATH_POINTS-1), points[i] );
	}

	// Add random short-lived particles from the gun tip to the target.
	while ( m_PathParticleEvent.NextEvent( flCur ) )
	{
		float t = RandomFloat( 0, 1 );
		int iPrev = (int)( t * (NUM_DRAINBEAM_PATH_POINTS - 1.001) );
		float tPrev = (float)iPrev / (NUM_DRAINBEAM_PATH_POINTS - 1);
		float tNext = (float)(iPrev+1) / (NUM_DRAINBEAM_PATH_POINTS - 1);
		Assert( tNext <= NUM_DRAINBEAM_PATH_POINTS-1 );

		Vector vPos;
		VectorLerp( points[iPrev], points[iPrev+1], (t-tPrev) / (tNext - tPrev), vPos );
		vPos += RandomVector( -2, 2 );
		
		SimpleParticle *pParticle = m_pEmitter->AddSimpleParticle( m_hParticleMaterial, vPos );
		if ( pParticle )
		{
			// Move the points along the path.
			pParticle->m_vecVelocity = points[iPrev+1] - points[iPrev];
			VectorNormalize( pParticle->m_vecVelocity );
			pParticle->m_vecVelocity *= -PARTICLE_PATH_VEL;

			pParticle->m_flRoll = 0;
			pParticle->m_flRollDelta = 0;
			pParticle->m_flDieTime = 0.2f;
			pParticle->m_flLifetime = 0;
			pParticle->m_uchColor[0] = 255; 
			pParticle->m_uchColor[1] = 255;
			pParticle->m_uchColor[2] = 255;
			pParticle->m_uchStartAlpha = 32;
			pParticle->m_uchEndAlpha = 0;
			pParticle->m_uchStartSize = 4;
			pParticle->m_uchEndSize = 2;
			pParticle->m_iFlags = 0;
		}
	}
}
#endif
