//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//=============================================================================//

#include "cbase.h"
#include "tf_weapon_passtime_gun.h"
#include "passtime_convars.h"
#include "in_buttons.h"
#include "tf_gamerules.h"
#ifdef GAME_DLL
#include "tf_passtime_ball.h"
#include "tf_passtime_logic.h"
#include "tf_player.h"
#include "tf_playerclass.h"
#include "tf_team.h"
#include "tf_gamestats.h"
#else // !GAME_DLL
#include "c_tf_passtime_logic.h"
#include "c_tf_passtime_ball.h"
#include "tf_hud_passtime_reticle.h"
#include "tf_viewmodel.h"
#include "c_tf_player.h"
#include "prediction.h"
#endif
#include "tier0/memdbgon.h"

//-----------------------------------------------------------------------------
IMPLEMENT_NETWORKCLASS_ALIASED( PasstimeGun, DT_PasstimeGun )

//-----------------------------------------------------------------------------
BEGIN_NETWORK_TABLE( CPasstimeGun, DT_PasstimeGun )
#ifdef GAME_DLL
	SendPropInt( SENDINFO( m_eThrowState ) ),
	SendPropFloat( SENDINFO( m_fChargeBeginTime ) )
#else
	RecvPropInt( RECVINFO( m_eThrowState ) ),
	RecvPropFloat( RECVINFO( m_fChargeBeginTime ) )
#endif
END_NETWORK_TABLE()

//-----------------------------------------------------------------------------
BEGIN_PREDICTION_DATA( CPasstimeGun )
END_PREDICTION_DATA() // this has to be here because the client's precache code uses it to get the classname of this entity...

//-----------------------------------------------------------------------------
LINK_ENTITY_TO_CLASS( tf_weapon_passtime_gun, CPasstimeGun );
PRECACHE_WEAPON_REGISTER( tf_weapon_passtime_gun );

//-----------------------------------------------------------------------------
namespace
{
	static char const * const kChargeSound = "Passtime.GunCharge";
	static char const * const kTargetHightlightSound = "Passtime.TargetLock";
	static char const * const kShootOkSound = "Passtime.Throw";
	static char const * const kPassOkSound = "Passtime.Throw";
	static char const * const kHalloweenBallModel = "models/passtime/ball/passtime_ball_halloween.mdl";
}

//-----------------------------------------------------------------------------
CPasstimeGun::CPasstimeGun()
	: m_flTargetResetTime( 0 )
	, m_attack( IN_ATTACK )
	, m_attack2( IN_ATTACK2 )
{
	m_eThrowState = THROWSTATE_DISABLED;
#ifdef CLIENT_DLL
	m_pBounceReticle = 0;
#endif
}

//-----------------------------------------------------------------------------
void CPasstimeGun::Spawn()
{
	m_iClip1 = -1;
	m_flTargetResetTime = 0;
	BaseClass::Spawn();

#ifdef CLIENT_DLL
	SetNextClientThink( CLIENT_THINK_ALWAYS );
	if( !m_pBounceReticle )
		m_pBounceReticle = new C_PasstimeBounceReticle();
#endif
}

//-----------------------------------------------------------------------------
CPasstimeGun::~CPasstimeGun()
{
#ifdef CLIENT_DLL
	delete m_pBounceReticle;
#endif
}

//-----------------------------------------------------------------------------
void CPasstimeGun::Equip( CBaseCombatCharacter *pOwner )
{
	// NOTE: This is not called on the client.

	// IsMarkedForDeletion can happen if the gun deletes itself in Spawn
	if ( IsMarkedForDeletion() )
	{
		return;
	}

	BaseClass::Equip( pOwner );
}

//-----------------------------------------------------------------------------
void CPasstimeGun::Precache()
{
	PrecacheScriptSound( kTargetHightlightSound );
	PrecacheScriptSound( kShootOkSound );
	PrecacheScriptSound( kPassOkSound );
	PrecacheScriptSound( kChargeSound );
	m_iAttachmentIndex = PrecacheModel( tf_passtime_ball_model.GetString() );
	if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) )
	{
		m_iHalloweenAttachmentIndex = PrecacheModel( kHalloweenBallModel );
	}
	else
	{
		m_iHalloweenAttachmentIndex = -1;
	}

	BaseClass::Precache();
}

//-----------------------------------------------------------------------------
bool CPasstimeGun::CanHolster() const
{
	return !GetTFPlayerOwner()->m_Shared.HasPasstimeBall();
}

//-----------------------------------------------------------------------------
bool CPasstimeGun::Holster( CBaseCombatWeapon *pSwitchingTo )
{
	// WeaponReset will always be called too
	return BaseClass::Holster( pSwitchingTo );
}

//-----------------------------------------------------------------------------
void CPasstimeGun::WeaponReset()
{
	// this can happen when the weapon is holstered or not
	BaseClass::WeaponReset();

	if ( (m_eThrowState != THROWSTATE_DISABLED) && (m_eThrowState != THROWSTATE_IDLE) )
	{
		m_eThrowState = THROWSTATE_CANCELLED;
		m_attack2.LatchUp();
		m_attack.LatchUp();
	}

#ifdef CLIENT_DLL
	if ( m_pBounceReticle )
		m_pBounceReticle->Hide();
#endif

	CTFPlayer* pOwner = GetTFPlayerOwner();
	if ( pOwner )
		pOwner->m_Shared.SetPasstimePassTarget( 0 );

}

//-----------------------------------------------------------------------------
#ifdef CLIENT_DLL
void CPasstimeGun::UpdateAttachmentModels()
{
	BaseClass::UpdateAttachmentModels();

	auto *pTFPlayer = GetTFPlayerOwner();
	if ( !pTFPlayer )
		return;

	if ( !pTFPlayer->IsLocalPlayer() )
		return;

	if ( !pTFPlayer->GetViewModel() )
		return;

	auto *pViewmodelBall = GetViewmodelAttachment();
	if ( !pViewmodelBall )
		return;

	auto iActiveIndex = pViewmodelBall->GetModelIndex();
	if ( m_iHalloweenAttachmentIndex != -1 ) 
	{
		if ( iActiveIndex != m_iHalloweenAttachmentIndex )
		{
			pViewmodelBall->SetModelIndex( m_iHalloweenAttachmentIndex );
			m_bAttachmentDirty = true;
		}
	}
	else if ( iActiveIndex != m_iAttachmentIndex )
	{
		pViewmodelBall->SetModelIndex( m_iAttachmentIndex );
		m_bAttachmentDirty = true;
	}
}
#endif

//-----------------------------------------------------------------------------
bool CPasstimeGun::CanCharge() // const
{
	return tf_passtime_experiment_instapass.GetBool()
		&& tf_passtime_experiment_instapass_charge.GetBool();
}

//-----------------------------------------------------------------------------
float CPasstimeGun::GetChargeBeginTime() 
{
	return m_fChargeBeginTime;
}

//-----------------------------------------------------------------------------
float CPasstimeGun::GetChargeMaxTime() 
{ 
	return (tf_passtime_experiment_instapass.GetBool() && tf_passtime_experiment_instapass_charge.GetBool())
		? 3.0f
		: 0.0f;
}

//-----------------------------------------------------------------------------
float CPasstimeGun::GetCurrentCharge()
{ 
	if ( (m_eThrowState == THROWSTATE_CHARGING) || (m_eThrowState == THROWSTATE_CHARGED) )
		return clamp((gpGlobals->curtime - GetChargeBeginTime()) / GetChargeMaxTime(), 0.0f, 1.0f);
	return 0;
}



//-----------------------------------------------------------------------------
void CPasstimeGun::UpdateOnRemove()
{
#ifdef CLIENT_DLL
	delete m_pBounceReticle;
	m_pBounceReticle = 0;
#endif
	BaseClass::UpdateOnRemove();
}

//-----------------------------------------------------------------------------
bool CPasstimeGun::VisibleInWeaponSelection() 
{
	return false;
}

static acttable_t s_acttablePasstime[] = 
{
	{ ACT_MP_STAND_IDLE,		ACT_MP_STAND_PASSTIME,			false },
	{ ACT_MP_RUN,				ACT_MP_RUN_PASSTIME,				false },
	{ ACT_MP_CROUCHWALK,		ACT_MP_CROUCHWALK_PASSTIME,		false },

	// the previous are the only actual unique ones
	
	// following is a copy from tf_weaponbase.cpp
	//acttable_t s_acttableMeleeAllclass[] = 
	//{
		//{ ACT_MP_STAND_IDLE,		ACT_MP_STAND_MELEE_ALLCLASS,				false },
	{ ACT_MP_CROUCH_IDLE,		ACT_MP_CROUCH_MELEE_ALLCLASS,			false },
		//{ ACT_MP_RUN,				ACT_MP_RUN_MELEE_ALLCLASS,				false },
	{ ACT_MP_WALK,				ACT_MP_WALK_MELEE_ALLCLASS,				false },
	{ ACT_MP_AIRWALK,			ACT_MP_AIRWALK_MELEE_ALLCLASS,			false },
		//{ ACT_MP_CROUCHWALK,		ACT_MP_CROUCHWALK_MELEE_ALLCLASS,		false },
	{ ACT_MP_JUMP,				ACT_MP_JUMP_MELEE_ALLCLASS,				false },
	{ ACT_MP_JUMP_START,		ACT_MP_JUMP_START_MELEE_ALLCLASS,		false },
	{ ACT_MP_JUMP_FLOAT,		ACT_MP_JUMP_FLOAT_MELEE_ALLCLASS,		false },
	{ ACT_MP_JUMP_LAND,			ACT_MP_JUMP_LAND_MELEE_ALLCLASS,			false },
	{ ACT_MP_SWIM,				ACT_MP_SWIM_MELEE_ALLCLASS,				false },
	{ ACT_MP_DOUBLEJUMP_CROUCH,	ACT_MP_DOUBLEJUMP_CROUCH_MELEE,	false },

	{ ACT_MP_ATTACK_STAND_PRIMARYFIRE,		ACT_MP_ATTACK_STAND_MELEE_ALLCLASS,		false },
	{ ACT_MP_ATTACK_CROUCH_PRIMARYFIRE,		ACT_MP_ATTACK_CROUCH_MELEE_ALLCLASS,		false },
	{ ACT_MP_ATTACK_SWIM_PRIMARYFIRE,		ACT_MP_ATTACK_SWIM_MELEE_ALLCLASS,		false },
	{ ACT_MP_ATTACK_AIRWALK_PRIMARYFIRE,	ACT_MP_ATTACK_AIRWALK_MELEE_ALLCLASS,	false },

	{ ACT_MP_ATTACK_STAND_SECONDARYFIRE,	ACT_MP_ATTACK_STAND_MELEE_SECONDARY, false },
	{ ACT_MP_ATTACK_CROUCH_SECONDARYFIRE,	ACT_MP_ATTACK_CROUCH_MELEE_SECONDARY,false },
	{ ACT_MP_ATTACK_SWIM_SECONDARYFIRE,		ACT_MP_ATTACK_SWIM_MELEE_ALLCLASS,		false },
	{ ACT_MP_ATTACK_AIRWALK_SECONDARYFIRE,	ACT_MP_ATTACK_AIRWALK_MELEE_ALLCLASS,	false },

	{ ACT_MP_GESTURE_FLINCH,	ACT_MP_GESTURE_FLINCH_MELEE, false },

	{ ACT_MP_GRENADE1_DRAW,		ACT_MP_MELEE_GRENADE1_DRAW,	false },
	{ ACT_MP_GRENADE1_IDLE,		ACT_MP_MELEE_GRENADE1_IDLE,	false },
	{ ACT_MP_GRENADE1_ATTACK,	ACT_MP_MELEE_GRENADE1_ATTACK,	false },
	{ ACT_MP_GRENADE2_DRAW,		ACT_MP_MELEE_GRENADE2_DRAW,	false },
	{ ACT_MP_GRENADE2_IDLE,		ACT_MP_MELEE_GRENADE2_IDLE,	false },
	{ ACT_MP_GRENADE2_ATTACK,	ACT_MP_MELEE_GRENADE2_ATTACK,	false },

	{ ACT_MP_GESTURE_VC_HANDMOUTH,	ACT_MP_GESTURE_VC_HANDMOUTH_MELEE,	false },
	{ ACT_MP_GESTURE_VC_FINGERPOINT,	ACT_MP_GESTURE_VC_FINGERPOINT_MELEE,	false },
	{ ACT_MP_GESTURE_VC_FISTPUMP,	ACT_MP_GESTURE_VC_FISTPUMP_MELEE,	false },
	{ ACT_MP_GESTURE_VC_THUMBSUP,	ACT_MP_GESTURE_VC_THUMBSUP_MELEE,	false },
	{ ACT_MP_GESTURE_VC_NODYES,	ACT_MP_GESTURE_VC_NODYES_MELEE,	false },
	{ ACT_MP_GESTURE_VC_NODNO,	ACT_MP_GESTURE_VC_NODNO_MELEE,	false },
};


//-----------------------------------------------------------------------------
acttable_t* CPasstimeGun::ActivityList(int &iActivityCount)
{
	iActivityCount = ARRAYSIZE(s_acttablePasstime);
	return GetTFPlayerOwner() 
		? s_acttablePasstime 
		: BaseClass::ActivityList(iActivityCount);
}

//-----------------------------------------------------------------------------
void CPasstimeGun::AttackInputState::Update( int held, int pressed, int released )
{
	if ( eButtonState == BUTTONSTATE_DISABLED )
	{
		return;
	}

	// this exists so i don't have to do lots of confusing "if button pressed and my 
	// charge timer is < curtime and some other bullshit then do this thing unless some
	// other variable says do something else".
	// note: can go directly from RELEASED to PRESSED without visiting UP along the way

	const bool bPressed = (pressed & iButton) == iButton;
	const bool bReleased = (released & iButton) == iButton;
	const bool bHeld = (held & iButton) == iButton;

	// if it's latched up, just keep reporting UP until the player releases the button
	if ( bLatchedUp )
	{
		if ( !bReleased )
		{
			eButtonState = BUTTONSTATE_UP;
			return;
		}
		else 
		{
			bLatchedUp = false;
		}
	}

	if ( bPressed )
	{
		eButtonState = BUTTONSTATE_PRESSED;
	}
	else if ( bReleased )
	{
		eButtonState = BUTTONSTATE_RELEASED;
	}
	else if ( bHeld ) 
	{
		eButtonState = BUTTONSTATE_DOWN;
	}
	else 
	{
		eButtonState = BUTTONSTATE_UP;
	}
}

//-----------------------------------------------------------------------------
void CPasstimeGun::AttackInputState::LatchUp()
{
	// can't use input->ClearButton here because we need this to apply on the server
	bLatchedUp = true;
	if ( eButtonState != BUTTONSTATE_UP )
		eButtonState = BUTTONSTATE_RELEASED;
}

//-----------------------------------------------------------------------------
void CPasstimeGun::AttackInputState::UnlatchUp()
{
	bLatchedUp = false;
}

//-----------------------------------------------------------------------------
bool CPasstimeGun::SendWeaponAnim( int actBase )
{
	switch ( actBase )
	{
	case ACT_VM_IDLE: 
		actBase = ACT_BALL_VM_IDLE;
		break;
	}

	return BaseClass::SendWeaponAnim( actBase );
}

//-----------------------------------------------------------------------------
void CPasstimeGun::ItemPostFrame()
{
	CTFPlayer *pOwner = ToTFPlayer( GetOwner() );
	if ( !pOwner )
	{
		return;
	}

	bool bCanAttack2Cancel = !tf_passtime_experiment_autopass.GetBool();

#ifdef GAME_DLL

	//
	// Update pass target
	//
	if ( pOwner->m_Shared.HasPasstimeBall() )
	{
		VMatrix mWorldToView( SetupMatrixIdentity() );
		Vector vecEyePos;
		{
			Vector vecEyeDir;
			pOwner->EyePositionAndVectors( &vecEyePos, &vecEyeDir, 0, 0 );
			const QAngle &angEye = pOwner->EyeAngles();
			const VMatrix mTemp( SetupMatrixOrgAngles( vecEyePos, angEye ) );
			MatrixInverseTR( mTemp, mWorldToView );
		}

		//
		// If the current target is behind me, forget it immediately
		//
		auto *pCurrentTarget = ToTFPlayer( pOwner->m_Shared.GetPasstimePassTarget() );
		if ( pCurrentTarget )
		{
			Vector vLocalCurrentTarget( 0, 0, 0 ); // current target in local space
			Vector3DMultiplyPosition( mWorldToView, pCurrentTarget->WorldSpaceCenter(), vLocalCurrentTarget );
			if ( vLocalCurrentTarget.x < 0 ) // behind me
			{
				// clear the target
				pOwner->m_Shared.SetPasstimePassTarget( 0 );
				m_flTargetResetTime = 0;
			}
		}

		//
		// Look for a pass target
		//
		auto bAutoPassing = tf_passtime_experiment_autopass.GetBool() && m_attack2.Is( EButtonState::BUTTONSTATE_DOWN );
		auto flBestTargetDist = bAutoPassing ? FLT_MAX : 0.1f;
		CTFPlayer *pNewTarget = nullptr;
		auto flMaxPassDistSqr = g_pPasstimeLogic->GetMaxPassRange();
		flMaxPassDistSqr *= flMaxPassDistSqr;
		if ( !bCanAttack2Cancel || !m_attack2.Is( BUTTONSTATE_DOWN ) ) // right click prevents pass lock
		{
			//
			// Find a valid pass target that's close to the center of the screen
			// When autopassing is happening, it's just world distance instead of viewspace distance
			//
			for( int i = 1; i <= MAX_PLAYERS; i++ )
			{
				auto *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
				if ( pPlayer == pOwner )
					continue; // skip self

				if ( !BValidPassTarget( pOwner, pPlayer ) )
					continue;

				// Check world distance
				const auto &vTargetPos = pPlayer->WorldSpaceCenter();
				auto flThisTargetDist = vTargetPos.DistToSqr(vecEyePos);
				if ( flThisTargetDist > flMaxPassDistSqr ) 
					continue;

				// Check viewspace distance from crosshair when not autopassing
				if ( !bAutoPassing )
				{
					Vector vLocalTarget;
					Vector3DMultiplyPosition( mWorldToView, vTargetPos, vLocalTarget );

					if ( vLocalTarget.x < 0 )
						continue; // behind me

					flThisTargetDist = Vector( -vLocalTarget.y / vLocalTarget.x, -vLocalTarget.z / vLocalTarget.x, 0 ).Length(); // not aspect-correct
				}

				// check if closer than best
				if ( flThisTargetDist >= flBestTargetDist )
					continue; // too far 

				// pretend that people who are asking for the ball are closer, so they get priority
				// do this after the distance check 
				if ( pPlayer->m_Shared.AskForBallTime() > gpGlobals->curtime )
					flThisTargetDist /= 50.0f;

				// check for line of sight
				trace_t tr;
				UTIL_TraceLine( vecEyePos,	vTargetPos, MASK_PLAYERSOLID, pOwner, COLLISION_GROUP_PROJECTILE, &tr );
				if ( tr.m_pEnt != pPlayer )
					continue; // obstructed

				// success - new target
				flBestTargetDist = flThisTargetDist;
				pNewTarget = pPlayer;
			}
		}

		//
		// Replace the current pass target with a better one
		//
		if ( pNewTarget )
		{
			// Always bump the target reset time when the target is valid.
			// When the target isn't under the cursor anymore, the reset time will try to 
			// keep the lock for a short amount of time.
			m_flTargetResetTime = gpGlobals->curtime + tf_passtime_mode_homing_lock_sec.GetFloat();

			if ( pNewTarget != pCurrentTarget )
			{
				pOwner->m_Shared.SetPasstimePassTarget( pNewTarget );
				pCurrentTarget = pNewTarget;

				// play the lock-on sound for the player
				CRecipientFilter filter;
				filter.AddRecipient( pOwner );
				EmitSound( filter, pOwner->entindex(), kTargetHightlightSound );

				// now play it for the target
				filter.RemoveAllRecipients();
				filter.AddRecipient( pCurrentTarget );
				EmitSound( filter, pCurrentTarget->entindex(), kTargetHightlightSound );
			}
		}
		//
		// See if the current pass target is still valid
		//
		else if ( pCurrentTarget 
			&& (!BValidPassTarget( pOwner, pCurrentTarget ) 
				|| (bCanAttack2Cancel && m_attack2.Is( BUTTONSTATE_DOWN )) // right click prevents pass lock
				|| ((m_flTargetResetTime > 0 ) && (m_flTargetResetTime < gpGlobals->curtime))
				|| (pCurrentTarget->WorldSpaceCenter().DistToSqr( vecEyePos ) >= flMaxPassDistSqr) )
			&& !m_attack.Is( BUTTONSTATE_DOWN ) ) // left click prevents pass unlock
		{
			pOwner->m_Shared.SetPasstimePassTarget( 0 );
			m_flTargetResetTime = 0;
		}

		// autopass
		if ( tf_passtime_experiment_autopass.GetBool() 
			&& m_attack2.Is( EButtonState::BUTTONSTATE_DOWN ) 
			&& pOwner->m_Shared.GetPasstimePassTarget() )
		{
			// NOTE: change state after calling Throw
			Throw( pOwner );
			m_eThrowState = THROWSTATE_THROWN;
			m_attack2.LatchUp();
			m_attack.LatchUp();
		}
	}
	else
	{
		//
		// Not carrying the ball
		//
		pOwner->m_Shared.SetPasstimePassTarget( 0 );
		m_flTargetResetTime = 0;
	}
#endif

	//
	// Update throw state
	// Client and server both run this code; client predicts everything ideally, but there are some 
	// sketchy bits in here that probably don't predict right.
	//
	if ( pOwner->m_Shared.HasPasstimeBall() )
	{
		if ( (m_eThrowState == THROWSTATE_DISABLED) || (m_flNextPrimaryAttack > gpGlobals->curtime) || !CanAttack() )
		{
			// disable the attack input so the state will be correct when 
			// throwstate changes to not disabled
			m_attack.Disable();
			m_attack2.Disable();
		}
		else 
		{
			// update input
			m_attack.Enable();
			m_attack.Update( pOwner->m_nButtons, pOwner->m_afButtonPressed, pOwner->m_afButtonReleased );
			m_attack2.Enable();
			m_attack2.Update( pOwner->m_nButtons, pOwner->m_afButtonPressed, pOwner->m_afButtonReleased );

			if ( bCanAttack2Cancel && m_attack2.Is( BUTTONSTATE_PRESSED ) )
			{
				// check for cancelling attack by pressing attack2
				if ( (m_eThrowState == THROWSTATE_CHARGING) || (m_eThrowState == THROWSTATE_CHARGED) )
				{
#ifdef GAME_DLL
					++CTF_GameStats.m_passtimeStats.summary.nTotalThrowCancels;
#endif
					m_eThrowState = THROWSTATE_CANCELLED;
					m_attack2.LatchUp();
					m_attack.LatchUp();
				}
				else 
				{
					pOwner->DoClassSpecialSkill();
				}
			}

			switch( m_eThrowState )
			{
			case THROWSTATE_IDLE:
			{
				if ( m_attack.Is( BUTTONSTATE_PRESSED ) )
				{
					// note: should transition to CHARGING even if it will immediately finish charging
					m_eThrowState = THROWSTATE_CHARGING;
					m_fChargeBeginTime = gpGlobals->curtime;
					if ( GetChargeMaxTime() != 0 )
					{
						EmitSound( kChargeSound );
					}
					SendWeaponAnim( ACT_BALL_VM_THROW_START );
					m_flThrowLoopStartTime = gpGlobals->curtime + SequenceDuration();
				}
				else
				{
					m_fChargeBeginTime = 0;
					WeaponIdle();
				}
				break;
			}

			case THROWSTATE_CHARGING: 
			{
				if ( m_attack.Is( BUTTONSTATE_RELEASED ) )
				{
					// NOTE: change state after calling Throw
					Throw( pOwner );
					m_eThrowState = THROWSTATE_THROWN;
					break;
				}

				if ( m_flThrowLoopStartTime < gpGlobals->curtime )
				{
					m_flThrowLoopStartTime = FLT_MAX;
					SendWeaponAnim( ACT_BALL_VM_THROW_LOOP );
				}

				if ( (m_fChargeBeginTime <= 0) || (GetCurrentCharge() >= 1) )
				{
					m_eThrowState = THROWSTATE_CHARGED;
				}
				break;
			}

			case THROWSTATE_CHARGED:
			{
				if ( m_attack.Is( BUTTONSTATE_RELEASED ) )
				{
					// NOTE: change state after calling Throw
					Throw( pOwner );
					m_eThrowState = THROWSTATE_THROWN;
				}

				if ( m_flThrowLoopStartTime < gpGlobals->curtime )
				{
					m_flThrowLoopStartTime = FLT_MAX;
					SendWeaponAnim( ACT_BALL_VM_THROW_LOOP );
				}
				break;
			}

			case THROWSTATE_CANCELLED:
			{
				m_eThrowState = THROWSTATE_IDLE;
				SendWeaponAnim( ACT_BALL_VM_THROW_END );
				m_flThrowLoopStartTime = FLT_MAX;
				StopSound( kChargeSound );
#ifdef GAME_DLL
				CPASAttenuationFilter filter( pOwner );
				pOwner->EmitSound( filter, pOwner->entindex(), kShootOkSound );
#endif
				break;
			}

			case THROWSTATE_THROWN: 
			{
				// This means you got the ball between throwing it and holstering the gun.
				// Just do what Deploy does, roughly.
				m_eThrowState = THROWSTATE_IDLE;
				m_attack2.LatchUp();
				m_attack.LatchUp();
				break; 
			}

			case THROWSTATE_DISABLED: // should never get here
			default:
				Warning( "Invalid EThrowState value" );
			};
		}
	}

	//
	// If the player doesn't have the ball, switch to the previous
	// weapon at an appropriate time
	// if the ball was thrown, wait a bit for animation to look better
	//
	if ( !pOwner->m_Shared.HasPasstimeBall()
		&& ((m_eThrowState != THROWSTATE_THROWN) || (m_flNextPrimaryAttack <= gpGlobals->curtime)) )
	{
		// Setting m_eThrowState here fixes players getting stuck in the throw 
		// anim when they lose the ball while charging to throw. See GetChargeBeginTime
		// and CTFPlayerAnimState::CheckPasstimeThrowAnimation to see why.
		m_eThrowState = THROWSTATE_IDLE; 

		if ( !m_hStoredLastWpn || !pOwner->Weapon_Switch( m_hStoredLastWpn ) )
		{
			pOwner->SwitchToNextBestWeapon( this );
		}
	}

	// this SetWeaponVisible should go away once we have real animations. if you remove this, 
	// update the EF_NODRAW hack in CTFWeaponBase::OnDataChanged too
	SetWeaponVisible( pOwner->m_Shared.HasPasstimeBall() );

#ifdef CLIENT_DLL
	if ( m_attack.Is( BUTTONSTATE_DOWN ) )
	{
		pOwner->SetFiredWeapon( true ); // not sure what this does, exactly, but it seems important
	}
#endif
}

//-----------------------------------------------------------------------------
#ifdef GAME_DLL
static const char* IncomingSoundForClass( const CTFPlayerClass* pClass, char (&pszSound)[64] )
{
	// note: this will probably need to be replaced with response rules
	pszSound[0] = 0;
	switch ( pClass->GetClassIndex() )
	{
	case TF_CLASS_SCOUT: 
		V_sprintf_safe( pszSound, "Scout.Incoming0%i", RandomInt(1,3) );
		return pszSound;

    case TF_CLASS_SNIPER:
		V_sprintf_safe( pszSound, "Sniper.Incoming0%i", RandomInt(1,4) );
		return pszSound;

    case TF_CLASS_SOLDIER:
		V_sprintf_safe( pszSound, "Soldier.Incoming01" );
		return pszSound;

	case TF_CLASS_DEMOMAN:
		V_sprintf_safe( pszSound, "Demoman.Incoming0%i", RandomInt(1,3) );
		return pszSound;

	case TF_CLASS_MEDIC:
		V_sprintf_safe( pszSound, "Medic.Incoming0%i", RandomInt(1,3) );
		return pszSound;

	case TF_CLASS_HEAVYWEAPONS:
		V_sprintf_safe( pszSound, "Heavy.Incoming0%i", RandomInt(1,3) );
		return pszSound;

	case TF_CLASS_PYRO:
		V_sprintf_safe( pszSound, "Pyro.Incoming01" );
		return pszSound;

	case TF_CLASS_SPY:
		V_sprintf_safe( pszSound, "Spy.Incoming0%i", RandomInt(1,3) );
		return pszSound;

	case TF_CLASS_ENGINEER:		
		V_sprintf_safe( pszSound, "Engineer.Incoming0%i", RandomInt(1,3) );
		return pszSound;
	};

	return pszSound;
}
#endif

//-----------------------------------------------------------------------------
void CPasstimeGun::Throw( CTFPlayer *pOwner )
{
	StopSound( kChargeSound );
	pOwner->SetAnimation( PLAYER_ATTACK1 );
	pOwner->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRIMARY );
	SendWeaponAnim( ACT_BALL_VM_THROW_END );
	m_flThrowLoopStartTime = FLT_MAX;

	m_flLastFireTime = gpGlobals->curtime;
	m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration(); // this prevents weapon switch until anim finishes
	m_flNextSecondaryAttack = m_flNextPrimaryAttack;

#ifdef GAME_DLL
	pOwner->NoteWeaponFired(); // not sure what this does, exactly, but it seems important 
	CTFPlayer *pPassTarget = pOwner->m_Shared.GetPasstimePassTarget();
	const LaunchParams& launch = CalcLaunch( pOwner, pPassTarget != 0 );
	g_pPasstimeLogic->LaunchBall( pOwner, launch.startPos, launch.startVel );

	CPASAttenuationFilter pasFilter( pOwner );
	pOwner->EmitSound( pasFilter, pOwner->entindex(), kShootOkSound );
	if ( pPassTarget )
	{
		++CTF_GameStats.m_passtimeStats.summary.nTotalPassesStarted;
		m_ballController.SetTargetSpeed( tf_passtime_mode_homing_speed.GetFloat() );
		auto isCharged = (m_fChargeBeginTime > 0) && (GetCurrentCharge() >= 1);
		m_ballController.StartHoming( g_pPasstimeLogic->GetBall(), pPassTarget, isCharged );
		if ( CTFPlayer *pPlayerPassTarget = ToTFPlayer( pPassTarget ) )
		{
			char pszSound[64];
			IncomingSoundForClass( pOwner->GetPlayerClass(), pszSound );
			{
				// for the thrower
				CRecipientFilter filter;
				filter.MakeReliable();
				filter.AddRecipient( pOwner );
				filter.AddRecipientsByTeam( TFTeamMgr()->GetTeam( TEAM_SPECTATOR ) );
				pOwner->EmitSound( filter, pOwner->entindex(), pszSound );
			}
			{
				// for the catcher
				CRecipientFilter filter;
				filter.MakeReliable();
				filter.AddRecipient( pPlayerPassTarget );
				pPlayerPassTarget->EmitSound( filter, pPlayerPassTarget->entindex(), pszSound );
			}
		}
	}
	else
	{
		++CTF_GameStats.m_passtimeStats.summary.nTotalTosses;
	}
#else
	pOwner->m_Shared.SetHasPasstimeBall( 0 ); // predict throwing
#endif

	pOwner->m_Shared.SetPasstimePassTarget( 0 );
}

//-----------------------------------------------------------------------------
void CPasstimeGun::ItemHolsterFrame()
{
	CTFPlayer *pOwner = ToTFPlayer( GetOwner() );
	if ( pOwner && pOwner->m_Shared.HasPasstimeBall() )
	{
		m_hStoredLastWpn = GetTFPlayerOwner()->GetActiveWeapon();
		pOwner->Weapon_Switch( this );
	}
}

//-----------------------------------------------------------------------------
const char *CPasstimeGun::GetWorldModel() const
{
	if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) )
	{
		return kHalloweenBallModel;
	}
	return tf_passtime_ball_model.GetString();
}

//-----------------------------------------------------------------------------
bool CPasstimeGun::Deploy()
{
	// This is not called on the client because the client can't predict it.
	if ( !BaseClass::Deploy() )
	{
		return false;
	}
	
	m_eThrowState = THROWSTATE_IDLE;
	m_attack2.UnlatchUp();
	m_attack.UnlatchUp();
	return true;
}


//-----------------------------------------------------------------------------
bool CPasstimeGun::CanDeploy()
{
	CTFPlayer *pOwner = GetTFPlayerOwner();
	return pOwner && pOwner->m_Shared.HasPasstimeBall() && BaseClass::CanDeploy();
}

//-----------------------------------------------------------------------------
// static
bool CPasstimeGun::BValidPassTarget( CTFPlayer *pSource, CTFPlayer *pTarget, HudNotification_t *pReason )
{
	if ( pReason ) *pReason = (HudNotification_t) 0;

	if ( !pTarget || (pTarget == pSource) )
	{
		return false;
	}

	bool bTargetDisguised = pTarget->m_Shared.InCond( TF_COND_DISGUISED );
	int iTargetTeam = pTarget->GetTeamNumber();
	int iSourceTeam = pSource ? pSource->GetTeamNumber() : iTargetTeam;
	bool bSameTeam = iTargetTeam == iSourceTeam;
	bool bTargetableEnemySpy = !bSameTeam && bTargetDisguised && (pTarget->m_Shared.GetDisguiseTeam() == iSourceTeam);

	if ( !bSameTeam && !bTargetableEnemySpy )
	{
		// can't pass to enemies
		return false; 
	}
	else if ( bSameTeam && bTargetDisguised )
	{
		// can't pass to disguised friendly spies
		if ( bTargetDisguised && pReason )
		{
			*pReason = HUD_NOTIFY_PASSTIME_NO_DISGUISE;
		}
		return false;
	}

#ifdef CLIENT_DLL
	return g_pPasstimeLogic->BCanPlayerPickUpBall( pTarget );
#else
	return g_pPasstimeLogic->BCanPlayerPickUpBall( pTarget, pReason );
#endif
}

//-----------------------------------------------------------------------------
#ifdef CLIENT_DLL
void CPasstimeGun::UpdateThrowArch()
{
	C_TFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() );
	if ( !pOwner )
	{
		return;
	}

	if ( !m_pBounceReticle )
	{
		m_pBounceReticle = new C_PasstimeBounceReticle();
	}

	if ( pOwner->m_Shared.GetPasstimePassTarget() )
	{
		m_pBounceReticle->Hide();
		return;
	}

	const LaunchParams& launchParams = CalcLaunch( pOwner, false );

	// Simple euler integration.
	// This seems to approximate what havok does reasonably accurately as long as there's no impact.
	Vector vecPos = launchParams.startPos;
	Vector vecVel = launchParams.startVel;

	const int iNumSuperSamples = 8;
	const float flDt = 1.0f / 16.0f / iNumSuperSamples;
	const Vector vecGravity_dt = flDt * Vector( 0, 0, -800 ); 
	const float flDamping_dt = flDt * tf_passtime_ball_damping_scale.GetFloat();

	Vector vecStart, vecEnd;
	trace_t tr;
	CTraceFilterSimple traceFilter( pOwner, COLLISION_GROUP_NONE );
	const int iMaxTraces = 100; // is this insane?
	for ( int iPoint = 0; iPoint < iMaxTraces; ++iPoint )
	{
		vecStart = vecPos;
		for ( int iSuperSample = 0; iSuperSample < iNumSuperSamples; ++iSuperSample )
		{
			vecVel += vecGravity_dt;
			vecVel -= vecVel * flDamping_dt;
			vecPos += vecVel * flDt;
		}
		vecEnd = vecPos;

		UTIL_TraceHull( vecStart, vecEnd, 
			-launchParams.traceHullSize, launchParams.traceHullSize, 
			MASK_PLAYERSOLID, &traceFilter, &tr );

		if ( tr.DidHit() )
		{	
			m_pBounceReticle->Show( tr.endpos, tr.plane.normal );
			break;

			// commented out code trying to guess bounce
			//vecVel = Lerp( tr.fraction, oldVel, vecVel ); // what vecVel was at point of impact, very roughly
			//vecPos = tr.endpos + tr.plane.normal; // move away from wall a bit
			//float speed = vecVel.NormalizeInPlace();
			//vecVel = -2 * vecVel.Dot( tr.plane.normal ) * tr.plane.normal + vecVel;
			//vecVel *= speed;
		}
	}

	if ( !tr.DidHit() )
	{
		m_pBounceReticle->Hide();
	}
}
#endif

//-----------------------------------------------------------------------------
//static 
CPasstimeGun::LaunchParams 
CPasstimeGun::LaunchParams::Default( CTFPlayer *pPlayer )
{
	LaunchParams p;
	pPlayer->EyePositionAndVectors( &p.eyePos, &p.viewFwd, &p.viewRight, &p.viewUp );
	const float size = tf_passtime_ball_sphere_radius.GetFloat() / 3.0f;
	p.traceHullSize = Vector( size, size, size );
	p.traceHullDistance = 8;
	p.startPos = pPlayer->Weapon_ShootPosition();
	p.startDir = p.viewFwd;
	p.startVel = p.startDir;
	return p;
}

//-----------------------------------------------------------------------------
static ConVar *s_pThrowSpeedConvars[TF_LAST_NORMAL_CLASS] = {
	nullptr, // TF_CLASS_UNDEFINED
	&tf_passtime_throwspeed_scout,
	&tf_passtime_throwspeed_sniper,
	&tf_passtime_throwspeed_soldier,
	&tf_passtime_throwspeed_demoman,
	&tf_passtime_throwspeed_medic,
	&tf_passtime_throwspeed_heavy,
	&tf_passtime_throwspeed_pyro,
	&tf_passtime_throwspeed_spy,
	&tf_passtime_throwspeed_engineer,
};

//-----------------------------------------------------------------------------
static ConVar *s_pThrowArcConvars[TF_LAST_NORMAL_CLASS] = {
	nullptr, // TF_CLASS_UNDEFINED
	&tf_passtime_throwarc_scout,
	&tf_passtime_throwarc_sniper,
	&tf_passtime_throwarc_soldier,
	&tf_passtime_throwarc_demoman,
	&tf_passtime_throwarc_medic,
	&tf_passtime_throwarc_heavy,
	&tf_passtime_throwarc_pyro,
	&tf_passtime_throwarc_spy,
	&tf_passtime_throwarc_engineer,
};

//-----------------------------------------------------------------------------
static void GetThrowParams( CTFPlayer *pPlayer, float *speed, float *arc )
{
	Assert( pPlayer && speed && arc );
	if ( !pPlayer ) return;

	auto iClass = pPlayer->GetPlayerClass()->GetClassIndex();
	if ( iClass <= TF_CLASS_UNDEFINED || iClass >= TF_LAST_NORMAL_CLASS ) 
	{
		if ( speed ) *speed = 1000.0f;
		if ( arc ) *arc = 0.3f;
	}
	else 
	{
		if ( speed ) *speed = s_pThrowSpeedConvars[iClass]->GetFloat();
		if ( arc ) *arc = s_pThrowArcConvars[iClass]->GetFloat();
	}
}

//-----------------------------------------------------------------------------
// static
CPasstimeGun::LaunchParams CPasstimeGun::CalcLaunch( CTFPlayer *pPlayer, bool bHoming )
{
	auto params = LaunchParams::Default( pPlayer );
	params.startPos = params.eyePos;

	if ( !bHoming )
	{
		float speed, arc;
		GetThrowParams( pPlayer, &speed, &arc );
		params.startVel = VectorLerp( params.startDir, Vector(0,0,1), arc );
		params.startVel.NormalizeInPlace();
		params.startVel *= speed;
	}
	else if ( !tf_passtime_experiment_autopass.GetBool() )
	{
		params.startVel = params.startDir * tf_passtime_mode_homing_speed.GetFloat();
	}
	else
	{
		params.startVel = Vector(0,0,0);
	}

	// mix in some amount of forward velocity
	auto fwdspeed = tf_passtime_throwspeed_velocity_scale.GetFloat() 
		* params.viewFwd.Dot( pPlayer->GetAbsVelocity() );
	VectorMAInline( params.startVel, fwdspeed, params.viewFwd, params.startVel );

	return params;
}

#ifdef CLIENT_DLL
//-----------------------------------------------------------------------------
void CPasstimeGun::ClientThink()
{
	if ( !IsActiveByLocalPlayer() && !IsLocalPlayerSpectator() )
	{
		if ( m_pBounceReticle )
		{
			m_pBounceReticle->Hide();
		}
		return;
	}

	// doing this in ItemPostFrame makes the position jittery for some reason, 
	// and doing it in ClientThink works better. Not entirely sure why, but I 
	// assume it's something to do with order of operations, or possibly prediction.
	if ( !IsLocalPlayerSpectator() && ((m_eThrowState == THROWSTATE_CHARGING) || (m_eThrowState == THROWSTATE_CHARGED)) )
	{
		UpdateThrowArch();
	}
	else if ( (IsLocalPlayerSpectator() || (m_eThrowState != THROWSTATE_THROWN)) && m_pBounceReticle )
	{
		m_pBounceReticle->Hide();
	}
}

#endif
