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