Team Fortress 2 Source Code as on 22/4/2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1107 lines
32 KiB

//========= 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