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

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose:
  4. //
  5. // $NoKeywords: $
  6. //=============================================================================//
  7. #include "cbase.h"
  8. #include "tf_weapon_passtime_gun.h"
  9. #include "passtime_convars.h"
  10. #include "in_buttons.h"
  11. #include "tf_gamerules.h"
  12. #ifdef GAME_DLL
  13. #include "tf_passtime_ball.h"
  14. #include "tf_passtime_logic.h"
  15. #include "tf_player.h"
  16. #include "tf_playerclass.h"
  17. #include "tf_team.h"
  18. #include "tf_gamestats.h"
  19. #else // !GAME_DLL
  20. #include "c_tf_passtime_logic.h"
  21. #include "c_tf_passtime_ball.h"
  22. #include "tf_hud_passtime_reticle.h"
  23. #include "tf_viewmodel.h"
  24. #include "c_tf_player.h"
  25. #include "prediction.h"
  26. #endif
  27. #include "tier0/memdbgon.h"
  28. //-----------------------------------------------------------------------------
  29. IMPLEMENT_NETWORKCLASS_ALIASED( PasstimeGun, DT_PasstimeGun )
  30. //-----------------------------------------------------------------------------
  31. BEGIN_NETWORK_TABLE( CPasstimeGun, DT_PasstimeGun )
  32. #ifdef GAME_DLL
  33. SendPropInt( SENDINFO( m_eThrowState ) ),
  34. SendPropFloat( SENDINFO( m_fChargeBeginTime ) )
  35. #else
  36. RecvPropInt( RECVINFO( m_eThrowState ) ),
  37. RecvPropFloat( RECVINFO( m_fChargeBeginTime ) )
  38. #endif
  39. END_NETWORK_TABLE()
  40. //-----------------------------------------------------------------------------
  41. BEGIN_PREDICTION_DATA( CPasstimeGun )
  42. END_PREDICTION_DATA() // this has to be here because the client's precache code uses it to get the classname of this entity...
  43. //-----------------------------------------------------------------------------
  44. LINK_ENTITY_TO_CLASS( tf_weapon_passtime_gun, CPasstimeGun );
  45. PRECACHE_WEAPON_REGISTER( tf_weapon_passtime_gun );
  46. //-----------------------------------------------------------------------------
  47. namespace
  48. {
  49. static char const * const kChargeSound = "Passtime.GunCharge";
  50. static char const * const kTargetHightlightSound = "Passtime.TargetLock";
  51. static char const * const kShootOkSound = "Passtime.Throw";
  52. static char const * const kPassOkSound = "Passtime.Throw";
  53. static char const * const kHalloweenBallModel = "models/passtime/ball/passtime_ball_halloween.mdl";
  54. }
  55. //-----------------------------------------------------------------------------
  56. CPasstimeGun::CPasstimeGun()
  57. : m_flTargetResetTime( 0 )
  58. , m_attack( IN_ATTACK )
  59. , m_attack2( IN_ATTACK2 )
  60. {
  61. m_eThrowState = THROWSTATE_DISABLED;
  62. #ifdef CLIENT_DLL
  63. m_pBounceReticle = 0;
  64. #endif
  65. }
  66. //-----------------------------------------------------------------------------
  67. void CPasstimeGun::Spawn()
  68. {
  69. m_iClip1 = -1;
  70. m_flTargetResetTime = 0;
  71. BaseClass::Spawn();
  72. #ifdef CLIENT_DLL
  73. SetNextClientThink( CLIENT_THINK_ALWAYS );
  74. if( !m_pBounceReticle )
  75. m_pBounceReticle = new C_PasstimeBounceReticle();
  76. #endif
  77. }
  78. //-----------------------------------------------------------------------------
  79. CPasstimeGun::~CPasstimeGun()
  80. {
  81. #ifdef CLIENT_DLL
  82. delete m_pBounceReticle;
  83. #endif
  84. }
  85. //-----------------------------------------------------------------------------
  86. void CPasstimeGun::Equip( CBaseCombatCharacter *pOwner )
  87. {
  88. // NOTE: This is not called on the client.
  89. // IsMarkedForDeletion can happen if the gun deletes itself in Spawn
  90. if ( IsMarkedForDeletion() )
  91. {
  92. return;
  93. }
  94. BaseClass::Equip( pOwner );
  95. }
  96. //-----------------------------------------------------------------------------
  97. void CPasstimeGun::Precache()
  98. {
  99. PrecacheScriptSound( kTargetHightlightSound );
  100. PrecacheScriptSound( kShootOkSound );
  101. PrecacheScriptSound( kPassOkSound );
  102. PrecacheScriptSound( kChargeSound );
  103. m_iAttachmentIndex = PrecacheModel( tf_passtime_ball_model.GetString() );
  104. if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) )
  105. {
  106. m_iHalloweenAttachmentIndex = PrecacheModel( kHalloweenBallModel );
  107. }
  108. else
  109. {
  110. m_iHalloweenAttachmentIndex = -1;
  111. }
  112. BaseClass::Precache();
  113. }
  114. //-----------------------------------------------------------------------------
  115. bool CPasstimeGun::CanHolster() const
  116. {
  117. return !GetTFPlayerOwner()->m_Shared.HasPasstimeBall();
  118. }
  119. //-----------------------------------------------------------------------------
  120. bool CPasstimeGun::Holster( CBaseCombatWeapon *pSwitchingTo )
  121. {
  122. // WeaponReset will always be called too
  123. return BaseClass::Holster( pSwitchingTo );
  124. }
  125. //-----------------------------------------------------------------------------
  126. void CPasstimeGun::WeaponReset()
  127. {
  128. // this can happen when the weapon is holstered or not
  129. BaseClass::WeaponReset();
  130. if ( (m_eThrowState != THROWSTATE_DISABLED) && (m_eThrowState != THROWSTATE_IDLE) )
  131. {
  132. m_eThrowState = THROWSTATE_CANCELLED;
  133. m_attack2.LatchUp();
  134. m_attack.LatchUp();
  135. }
  136. #ifdef CLIENT_DLL
  137. if ( m_pBounceReticle )
  138. m_pBounceReticle->Hide();
  139. #endif
  140. CTFPlayer* pOwner = GetTFPlayerOwner();
  141. if ( pOwner )
  142. pOwner->m_Shared.SetPasstimePassTarget( 0 );
  143. }
  144. //-----------------------------------------------------------------------------
  145. #ifdef CLIENT_DLL
  146. void CPasstimeGun::UpdateAttachmentModels()
  147. {
  148. BaseClass::UpdateAttachmentModels();
  149. auto *pTFPlayer = GetTFPlayerOwner();
  150. if ( !pTFPlayer )
  151. return;
  152. if ( !pTFPlayer->IsLocalPlayer() )
  153. return;
  154. if ( !pTFPlayer->GetViewModel() )
  155. return;
  156. auto *pViewmodelBall = GetViewmodelAttachment();
  157. if ( !pViewmodelBall )
  158. return;
  159. auto iActiveIndex = pViewmodelBall->GetModelIndex();
  160. if ( m_iHalloweenAttachmentIndex != -1 )
  161. {
  162. if ( iActiveIndex != m_iHalloweenAttachmentIndex )
  163. {
  164. pViewmodelBall->SetModelIndex( m_iHalloweenAttachmentIndex );
  165. m_bAttachmentDirty = true;
  166. }
  167. }
  168. else if ( iActiveIndex != m_iAttachmentIndex )
  169. {
  170. pViewmodelBall->SetModelIndex( m_iAttachmentIndex );
  171. m_bAttachmentDirty = true;
  172. }
  173. }
  174. #endif
  175. //-----------------------------------------------------------------------------
  176. bool CPasstimeGun::CanCharge() // const
  177. {
  178. return tf_passtime_experiment_instapass.GetBool()
  179. && tf_passtime_experiment_instapass_charge.GetBool();
  180. }
  181. //-----------------------------------------------------------------------------
  182. float CPasstimeGun::GetChargeBeginTime()
  183. {
  184. return m_fChargeBeginTime;
  185. }
  186. //-----------------------------------------------------------------------------
  187. float CPasstimeGun::GetChargeMaxTime()
  188. {
  189. return (tf_passtime_experiment_instapass.GetBool() && tf_passtime_experiment_instapass_charge.GetBool())
  190. ? 3.0f
  191. : 0.0f;
  192. }
  193. //-----------------------------------------------------------------------------
  194. float CPasstimeGun::GetCurrentCharge()
  195. {
  196. if ( (m_eThrowState == THROWSTATE_CHARGING) || (m_eThrowState == THROWSTATE_CHARGED) )
  197. return clamp((gpGlobals->curtime - GetChargeBeginTime()) / GetChargeMaxTime(), 0.0f, 1.0f);
  198. return 0;
  199. }
  200. //-----------------------------------------------------------------------------
  201. void CPasstimeGun::UpdateOnRemove()
  202. {
  203. #ifdef CLIENT_DLL
  204. delete m_pBounceReticle;
  205. m_pBounceReticle = 0;
  206. #endif
  207. BaseClass::UpdateOnRemove();
  208. }
  209. //-----------------------------------------------------------------------------
  210. bool CPasstimeGun::VisibleInWeaponSelection()
  211. {
  212. return false;
  213. }
  214. static acttable_t s_acttablePasstime[] =
  215. {
  216. { ACT_MP_STAND_IDLE, ACT_MP_STAND_PASSTIME, false },
  217. { ACT_MP_RUN, ACT_MP_RUN_PASSTIME, false },
  218. { ACT_MP_CROUCHWALK, ACT_MP_CROUCHWALK_PASSTIME, false },
  219. // the previous are the only actual unique ones
  220. // following is a copy from tf_weaponbase.cpp
  221. //acttable_t s_acttableMeleeAllclass[] =
  222. //{
  223. //{ ACT_MP_STAND_IDLE, ACT_MP_STAND_MELEE_ALLCLASS, false },
  224. { ACT_MP_CROUCH_IDLE, ACT_MP_CROUCH_MELEE_ALLCLASS, false },
  225. //{ ACT_MP_RUN, ACT_MP_RUN_MELEE_ALLCLASS, false },
  226. { ACT_MP_WALK, ACT_MP_WALK_MELEE_ALLCLASS, false },
  227. { ACT_MP_AIRWALK, ACT_MP_AIRWALK_MELEE_ALLCLASS, false },
  228. //{ ACT_MP_CROUCHWALK, ACT_MP_CROUCHWALK_MELEE_ALLCLASS, false },
  229. { ACT_MP_JUMP, ACT_MP_JUMP_MELEE_ALLCLASS, false },
  230. { ACT_MP_JUMP_START, ACT_MP_JUMP_START_MELEE_ALLCLASS, false },
  231. { ACT_MP_JUMP_FLOAT, ACT_MP_JUMP_FLOAT_MELEE_ALLCLASS, false },
  232. { ACT_MP_JUMP_LAND, ACT_MP_JUMP_LAND_MELEE_ALLCLASS, false },
  233. { ACT_MP_SWIM, ACT_MP_SWIM_MELEE_ALLCLASS, false },
  234. { ACT_MP_DOUBLEJUMP_CROUCH, ACT_MP_DOUBLEJUMP_CROUCH_MELEE, false },
  235. { ACT_MP_ATTACK_STAND_PRIMARYFIRE, ACT_MP_ATTACK_STAND_MELEE_ALLCLASS, false },
  236. { ACT_MP_ATTACK_CROUCH_PRIMARYFIRE, ACT_MP_ATTACK_CROUCH_MELEE_ALLCLASS, false },
  237. { ACT_MP_ATTACK_SWIM_PRIMARYFIRE, ACT_MP_ATTACK_SWIM_MELEE_ALLCLASS, false },
  238. { ACT_MP_ATTACK_AIRWALK_PRIMARYFIRE, ACT_MP_ATTACK_AIRWALK_MELEE_ALLCLASS, false },
  239. { ACT_MP_ATTACK_STAND_SECONDARYFIRE, ACT_MP_ATTACK_STAND_MELEE_SECONDARY, false },
  240. { ACT_MP_ATTACK_CROUCH_SECONDARYFIRE, ACT_MP_ATTACK_CROUCH_MELEE_SECONDARY,false },
  241. { ACT_MP_ATTACK_SWIM_SECONDARYFIRE, ACT_MP_ATTACK_SWIM_MELEE_ALLCLASS, false },
  242. { ACT_MP_ATTACK_AIRWALK_SECONDARYFIRE, ACT_MP_ATTACK_AIRWALK_MELEE_ALLCLASS, false },
  243. { ACT_MP_GESTURE_FLINCH, ACT_MP_GESTURE_FLINCH_MELEE, false },
  244. { ACT_MP_GRENADE1_DRAW, ACT_MP_MELEE_GRENADE1_DRAW, false },
  245. { ACT_MP_GRENADE1_IDLE, ACT_MP_MELEE_GRENADE1_IDLE, false },
  246. { ACT_MP_GRENADE1_ATTACK, ACT_MP_MELEE_GRENADE1_ATTACK, false },
  247. { ACT_MP_GRENADE2_DRAW, ACT_MP_MELEE_GRENADE2_DRAW, false },
  248. { ACT_MP_GRENADE2_IDLE, ACT_MP_MELEE_GRENADE2_IDLE, false },
  249. { ACT_MP_GRENADE2_ATTACK, ACT_MP_MELEE_GRENADE2_ATTACK, false },
  250. { ACT_MP_GESTURE_VC_HANDMOUTH, ACT_MP_GESTURE_VC_HANDMOUTH_MELEE, false },
  251. { ACT_MP_GESTURE_VC_FINGERPOINT, ACT_MP_GESTURE_VC_FINGERPOINT_MELEE, false },
  252. { ACT_MP_GESTURE_VC_FISTPUMP, ACT_MP_GESTURE_VC_FISTPUMP_MELEE, false },
  253. { ACT_MP_GESTURE_VC_THUMBSUP, ACT_MP_GESTURE_VC_THUMBSUP_MELEE, false },
  254. { ACT_MP_GESTURE_VC_NODYES, ACT_MP_GESTURE_VC_NODYES_MELEE, false },
  255. { ACT_MP_GESTURE_VC_NODNO, ACT_MP_GESTURE_VC_NODNO_MELEE, false },
  256. };
  257. //-----------------------------------------------------------------------------
  258. acttable_t* CPasstimeGun::ActivityList(int &iActivityCount)
  259. {
  260. iActivityCount = ARRAYSIZE(s_acttablePasstime);
  261. return GetTFPlayerOwner()
  262. ? s_acttablePasstime
  263. : BaseClass::ActivityList(iActivityCount);
  264. }
  265. //-----------------------------------------------------------------------------
  266. void CPasstimeGun::AttackInputState::Update( int held, int pressed, int released )
  267. {
  268. if ( eButtonState == BUTTONSTATE_DISABLED )
  269. {
  270. return;
  271. }
  272. // this exists so i don't have to do lots of confusing "if button pressed and my
  273. // charge timer is < curtime and some other bullshit then do this thing unless some
  274. // other variable says do something else".
  275. // note: can go directly from RELEASED to PRESSED without visiting UP along the way
  276. const bool bPressed = (pressed & iButton) == iButton;
  277. const bool bReleased = (released & iButton) == iButton;
  278. const bool bHeld = (held & iButton) == iButton;
  279. // if it's latched up, just keep reporting UP until the player releases the button
  280. if ( bLatchedUp )
  281. {
  282. if ( !bReleased )
  283. {
  284. eButtonState = BUTTONSTATE_UP;
  285. return;
  286. }
  287. else
  288. {
  289. bLatchedUp = false;
  290. }
  291. }
  292. if ( bPressed )
  293. {
  294. eButtonState = BUTTONSTATE_PRESSED;
  295. }
  296. else if ( bReleased )
  297. {
  298. eButtonState = BUTTONSTATE_RELEASED;
  299. }
  300. else if ( bHeld )
  301. {
  302. eButtonState = BUTTONSTATE_DOWN;
  303. }
  304. else
  305. {
  306. eButtonState = BUTTONSTATE_UP;
  307. }
  308. }
  309. //-----------------------------------------------------------------------------
  310. void CPasstimeGun::AttackInputState::LatchUp()
  311. {
  312. // can't use input->ClearButton here because we need this to apply on the server
  313. bLatchedUp = true;
  314. if ( eButtonState != BUTTONSTATE_UP )
  315. eButtonState = BUTTONSTATE_RELEASED;
  316. }
  317. //-----------------------------------------------------------------------------
  318. void CPasstimeGun::AttackInputState::UnlatchUp()
  319. {
  320. bLatchedUp = false;
  321. }
  322. //-----------------------------------------------------------------------------
  323. bool CPasstimeGun::SendWeaponAnim( int actBase )
  324. {
  325. switch ( actBase )
  326. {
  327. case ACT_VM_IDLE:
  328. actBase = ACT_BALL_VM_IDLE;
  329. break;
  330. }
  331. return BaseClass::SendWeaponAnim( actBase );
  332. }
  333. //-----------------------------------------------------------------------------
  334. void CPasstimeGun::ItemPostFrame()
  335. {
  336. CTFPlayer *pOwner = ToTFPlayer( GetOwner() );
  337. if ( !pOwner )
  338. {
  339. return;
  340. }
  341. bool bCanAttack2Cancel = !tf_passtime_experiment_autopass.GetBool();
  342. #ifdef GAME_DLL
  343. //
  344. // Update pass target
  345. //
  346. if ( pOwner->m_Shared.HasPasstimeBall() )
  347. {
  348. VMatrix mWorldToView( SetupMatrixIdentity() );
  349. Vector vecEyePos;
  350. {
  351. Vector vecEyeDir;
  352. pOwner->EyePositionAndVectors( &vecEyePos, &vecEyeDir, 0, 0 );
  353. const QAngle &angEye = pOwner->EyeAngles();
  354. const VMatrix mTemp( SetupMatrixOrgAngles( vecEyePos, angEye ) );
  355. MatrixInverseTR( mTemp, mWorldToView );
  356. }
  357. //
  358. // If the current target is behind me, forget it immediately
  359. //
  360. auto *pCurrentTarget = ToTFPlayer( pOwner->m_Shared.GetPasstimePassTarget() );
  361. if ( pCurrentTarget )
  362. {
  363. Vector vLocalCurrentTarget( 0, 0, 0 ); // current target in local space
  364. Vector3DMultiplyPosition( mWorldToView, pCurrentTarget->WorldSpaceCenter(), vLocalCurrentTarget );
  365. if ( vLocalCurrentTarget.x < 0 ) // behind me
  366. {
  367. // clear the target
  368. pOwner->m_Shared.SetPasstimePassTarget( 0 );
  369. m_flTargetResetTime = 0;
  370. }
  371. }
  372. //
  373. // Look for a pass target
  374. //
  375. auto bAutoPassing = tf_passtime_experiment_autopass.GetBool() && m_attack2.Is( EButtonState::BUTTONSTATE_DOWN );
  376. auto flBestTargetDist = bAutoPassing ? FLT_MAX : 0.1f;
  377. CTFPlayer *pNewTarget = nullptr;
  378. auto flMaxPassDistSqr = g_pPasstimeLogic->GetMaxPassRange();
  379. flMaxPassDistSqr *= flMaxPassDistSqr;
  380. if ( !bCanAttack2Cancel || !m_attack2.Is( BUTTONSTATE_DOWN ) ) // right click prevents pass lock
  381. {
  382. //
  383. // Find a valid pass target that's close to the center of the screen
  384. // When autopassing is happening, it's just world distance instead of viewspace distance
  385. //
  386. for( int i = 1; i <= MAX_PLAYERS; i++ )
  387. {
  388. auto *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
  389. if ( pPlayer == pOwner )
  390. continue; // skip self
  391. if ( !BValidPassTarget( pOwner, pPlayer ) )
  392. continue;
  393. // Check world distance
  394. const auto &vTargetPos = pPlayer->WorldSpaceCenter();
  395. auto flThisTargetDist = vTargetPos.DistToSqr(vecEyePos);
  396. if ( flThisTargetDist > flMaxPassDistSqr )
  397. continue;
  398. // Check viewspace distance from crosshair when not autopassing
  399. if ( !bAutoPassing )
  400. {
  401. Vector vLocalTarget;
  402. Vector3DMultiplyPosition( mWorldToView, vTargetPos, vLocalTarget );
  403. if ( vLocalTarget.x < 0 )
  404. continue; // behind me
  405. flThisTargetDist = Vector( -vLocalTarget.y / vLocalTarget.x, -vLocalTarget.z / vLocalTarget.x, 0 ).Length(); // not aspect-correct
  406. }
  407. // check if closer than best
  408. if ( flThisTargetDist >= flBestTargetDist )
  409. continue; // too far
  410. // pretend that people who are asking for the ball are closer, so they get priority
  411. // do this after the distance check
  412. if ( pPlayer->m_Shared.AskForBallTime() > gpGlobals->curtime )
  413. flThisTargetDist /= 50.0f;
  414. // check for line of sight
  415. trace_t tr;
  416. UTIL_TraceLine( vecEyePos, vTargetPos, MASK_PLAYERSOLID, pOwner, COLLISION_GROUP_PROJECTILE, &tr );
  417. if ( tr.m_pEnt != pPlayer )
  418. continue; // obstructed
  419. // success - new target
  420. flBestTargetDist = flThisTargetDist;
  421. pNewTarget = pPlayer;
  422. }
  423. }
  424. //
  425. // Replace the current pass target with a better one
  426. //
  427. if ( pNewTarget )
  428. {
  429. // Always bump the target reset time when the target is valid.
  430. // When the target isn't under the cursor anymore, the reset time will try to
  431. // keep the lock for a short amount of time.
  432. m_flTargetResetTime = gpGlobals->curtime + tf_passtime_mode_homing_lock_sec.GetFloat();
  433. if ( pNewTarget != pCurrentTarget )
  434. {
  435. pOwner->m_Shared.SetPasstimePassTarget( pNewTarget );
  436. pCurrentTarget = pNewTarget;
  437. // play the lock-on sound for the player
  438. CRecipientFilter filter;
  439. filter.AddRecipient( pOwner );
  440. EmitSound( filter, pOwner->entindex(), kTargetHightlightSound );
  441. // now play it for the target
  442. filter.RemoveAllRecipients();
  443. filter.AddRecipient( pCurrentTarget );
  444. EmitSound( filter, pCurrentTarget->entindex(), kTargetHightlightSound );
  445. }
  446. }
  447. //
  448. // See if the current pass target is still valid
  449. //
  450. else if ( pCurrentTarget
  451. && (!BValidPassTarget( pOwner, pCurrentTarget )
  452. || (bCanAttack2Cancel && m_attack2.Is( BUTTONSTATE_DOWN )) // right click prevents pass lock
  453. || ((m_flTargetResetTime > 0 ) && (m_flTargetResetTime < gpGlobals->curtime))
  454. || (pCurrentTarget->WorldSpaceCenter().DistToSqr( vecEyePos ) >= flMaxPassDistSqr) )
  455. && !m_attack.Is( BUTTONSTATE_DOWN ) ) // left click prevents pass unlock
  456. {
  457. pOwner->m_Shared.SetPasstimePassTarget( 0 );
  458. m_flTargetResetTime = 0;
  459. }
  460. // autopass
  461. if ( tf_passtime_experiment_autopass.GetBool()
  462. && m_attack2.Is( EButtonState::BUTTONSTATE_DOWN )
  463. && pOwner->m_Shared.GetPasstimePassTarget() )
  464. {
  465. // NOTE: change state after calling Throw
  466. Throw( pOwner );
  467. m_eThrowState = THROWSTATE_THROWN;
  468. m_attack2.LatchUp();
  469. m_attack.LatchUp();
  470. }
  471. }
  472. else
  473. {
  474. //
  475. // Not carrying the ball
  476. //
  477. pOwner->m_Shared.SetPasstimePassTarget( 0 );
  478. m_flTargetResetTime = 0;
  479. }
  480. #endif
  481. //
  482. // Update throw state
  483. // Client and server both run this code; client predicts everything ideally, but there are some
  484. // sketchy bits in here that probably don't predict right.
  485. //
  486. if ( pOwner->m_Shared.HasPasstimeBall() )
  487. {
  488. if ( (m_eThrowState == THROWSTATE_DISABLED) || (m_flNextPrimaryAttack > gpGlobals->curtime) || !CanAttack() )
  489. {
  490. // disable the attack input so the state will be correct when
  491. // throwstate changes to not disabled
  492. m_attack.Disable();
  493. m_attack2.Disable();
  494. }
  495. else
  496. {
  497. // update input
  498. m_attack.Enable();
  499. m_attack.Update( pOwner->m_nButtons, pOwner->m_afButtonPressed, pOwner->m_afButtonReleased );
  500. m_attack2.Enable();
  501. m_attack2.Update( pOwner->m_nButtons, pOwner->m_afButtonPressed, pOwner->m_afButtonReleased );
  502. if ( bCanAttack2Cancel && m_attack2.Is( BUTTONSTATE_PRESSED ) )
  503. {
  504. // check for cancelling attack by pressing attack2
  505. if ( (m_eThrowState == THROWSTATE_CHARGING) || (m_eThrowState == THROWSTATE_CHARGED) )
  506. {
  507. #ifdef GAME_DLL
  508. ++CTF_GameStats.m_passtimeStats.summary.nTotalThrowCancels;
  509. #endif
  510. m_eThrowState = THROWSTATE_CANCELLED;
  511. m_attack2.LatchUp();
  512. m_attack.LatchUp();
  513. }
  514. else
  515. {
  516. pOwner->DoClassSpecialSkill();
  517. }
  518. }
  519. switch( m_eThrowState )
  520. {
  521. case THROWSTATE_IDLE:
  522. {
  523. if ( m_attack.Is( BUTTONSTATE_PRESSED ) )
  524. {
  525. // note: should transition to CHARGING even if it will immediately finish charging
  526. m_eThrowState = THROWSTATE_CHARGING;
  527. m_fChargeBeginTime = gpGlobals->curtime;
  528. if ( GetChargeMaxTime() != 0 )
  529. {
  530. EmitSound( kChargeSound );
  531. }
  532. SendWeaponAnim( ACT_BALL_VM_THROW_START );
  533. m_flThrowLoopStartTime = gpGlobals->curtime + SequenceDuration();
  534. }
  535. else
  536. {
  537. m_fChargeBeginTime = 0;
  538. WeaponIdle();
  539. }
  540. break;
  541. }
  542. case THROWSTATE_CHARGING:
  543. {
  544. if ( m_attack.Is( BUTTONSTATE_RELEASED ) )
  545. {
  546. // NOTE: change state after calling Throw
  547. Throw( pOwner );
  548. m_eThrowState = THROWSTATE_THROWN;
  549. break;
  550. }
  551. if ( m_flThrowLoopStartTime < gpGlobals->curtime )
  552. {
  553. m_flThrowLoopStartTime = FLT_MAX;
  554. SendWeaponAnim( ACT_BALL_VM_THROW_LOOP );
  555. }
  556. if ( (m_fChargeBeginTime <= 0) || (GetCurrentCharge() >= 1) )
  557. {
  558. m_eThrowState = THROWSTATE_CHARGED;
  559. }
  560. break;
  561. }
  562. case THROWSTATE_CHARGED:
  563. {
  564. if ( m_attack.Is( BUTTONSTATE_RELEASED ) )
  565. {
  566. // NOTE: change state after calling Throw
  567. Throw( pOwner );
  568. m_eThrowState = THROWSTATE_THROWN;
  569. }
  570. if ( m_flThrowLoopStartTime < gpGlobals->curtime )
  571. {
  572. m_flThrowLoopStartTime = FLT_MAX;
  573. SendWeaponAnim( ACT_BALL_VM_THROW_LOOP );
  574. }
  575. break;
  576. }
  577. case THROWSTATE_CANCELLED:
  578. {
  579. m_eThrowState = THROWSTATE_IDLE;
  580. SendWeaponAnim( ACT_BALL_VM_THROW_END );
  581. m_flThrowLoopStartTime = FLT_MAX;
  582. StopSound( kChargeSound );
  583. #ifdef GAME_DLL
  584. CPASAttenuationFilter filter( pOwner );
  585. pOwner->EmitSound( filter, pOwner->entindex(), kShootOkSound );
  586. #endif
  587. break;
  588. }
  589. case THROWSTATE_THROWN:
  590. {
  591. // This means you got the ball between throwing it and holstering the gun.
  592. // Just do what Deploy does, roughly.
  593. m_eThrowState = THROWSTATE_IDLE;
  594. m_attack2.LatchUp();
  595. m_attack.LatchUp();
  596. break;
  597. }
  598. case THROWSTATE_DISABLED: // should never get here
  599. default:
  600. Warning( "Invalid EThrowState value" );
  601. };
  602. }
  603. }
  604. //
  605. // If the player doesn't have the ball, switch to the previous
  606. // weapon at an appropriate time
  607. // if the ball was thrown, wait a bit for animation to look better
  608. //
  609. if ( !pOwner->m_Shared.HasPasstimeBall()
  610. && ((m_eThrowState != THROWSTATE_THROWN) || (m_flNextPrimaryAttack <= gpGlobals->curtime)) )
  611. {
  612. // Setting m_eThrowState here fixes players getting stuck in the throw
  613. // anim when they lose the ball while charging to throw. See GetChargeBeginTime
  614. // and CTFPlayerAnimState::CheckPasstimeThrowAnimation to see why.
  615. m_eThrowState = THROWSTATE_IDLE;
  616. if ( !m_hStoredLastWpn || !pOwner->Weapon_Switch( m_hStoredLastWpn ) )
  617. {
  618. pOwner->SwitchToNextBestWeapon( this );
  619. }
  620. }
  621. // this SetWeaponVisible should go away once we have real animations. if you remove this,
  622. // update the EF_NODRAW hack in CTFWeaponBase::OnDataChanged too
  623. SetWeaponVisible( pOwner->m_Shared.HasPasstimeBall() );
  624. #ifdef CLIENT_DLL
  625. if ( m_attack.Is( BUTTONSTATE_DOWN ) )
  626. {
  627. pOwner->SetFiredWeapon( true ); // not sure what this does, exactly, but it seems important
  628. }
  629. #endif
  630. }
  631. //-----------------------------------------------------------------------------
  632. #ifdef GAME_DLL
  633. static const char* IncomingSoundForClass( const CTFPlayerClass* pClass, char (&pszSound)[64] )
  634. {
  635. // note: this will probably need to be replaced with response rules
  636. pszSound[0] = 0;
  637. switch ( pClass->GetClassIndex() )
  638. {
  639. case TF_CLASS_SCOUT:
  640. V_sprintf_safe( pszSound, "Scout.Incoming0%i", RandomInt(1,3) );
  641. return pszSound;
  642. case TF_CLASS_SNIPER:
  643. V_sprintf_safe( pszSound, "Sniper.Incoming0%i", RandomInt(1,4) );
  644. return pszSound;
  645. case TF_CLASS_SOLDIER:
  646. V_sprintf_safe( pszSound, "Soldier.Incoming01" );
  647. return pszSound;
  648. case TF_CLASS_DEMOMAN:
  649. V_sprintf_safe( pszSound, "Demoman.Incoming0%i", RandomInt(1,3) );
  650. return pszSound;
  651. case TF_CLASS_MEDIC:
  652. V_sprintf_safe( pszSound, "Medic.Incoming0%i", RandomInt(1,3) );
  653. return pszSound;
  654. case TF_CLASS_HEAVYWEAPONS:
  655. V_sprintf_safe( pszSound, "Heavy.Incoming0%i", RandomInt(1,3) );
  656. return pszSound;
  657. case TF_CLASS_PYRO:
  658. V_sprintf_safe( pszSound, "Pyro.Incoming01" );
  659. return pszSound;
  660. case TF_CLASS_SPY:
  661. V_sprintf_safe( pszSound, "Spy.Incoming0%i", RandomInt(1,3) );
  662. return pszSound;
  663. case TF_CLASS_ENGINEER:
  664. V_sprintf_safe( pszSound, "Engineer.Incoming0%i", RandomInt(1,3) );
  665. return pszSound;
  666. };
  667. return pszSound;
  668. }
  669. #endif
  670. //-----------------------------------------------------------------------------
  671. void CPasstimeGun::Throw( CTFPlayer *pOwner )
  672. {
  673. StopSound( kChargeSound );
  674. pOwner->SetAnimation( PLAYER_ATTACK1 );
  675. pOwner->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRIMARY );
  676. SendWeaponAnim( ACT_BALL_VM_THROW_END );
  677. m_flThrowLoopStartTime = FLT_MAX;
  678. m_flLastFireTime = gpGlobals->curtime;
  679. m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration(); // this prevents weapon switch until anim finishes
  680. m_flNextSecondaryAttack = m_flNextPrimaryAttack;
  681. #ifdef GAME_DLL
  682. pOwner->NoteWeaponFired(); // not sure what this does, exactly, but it seems important
  683. CTFPlayer *pPassTarget = pOwner->m_Shared.GetPasstimePassTarget();
  684. const LaunchParams& launch = CalcLaunch( pOwner, pPassTarget != 0 );
  685. g_pPasstimeLogic->LaunchBall( pOwner, launch.startPos, launch.startVel );
  686. CPASAttenuationFilter pasFilter( pOwner );
  687. pOwner->EmitSound( pasFilter, pOwner->entindex(), kShootOkSound );
  688. if ( pPassTarget )
  689. {
  690. ++CTF_GameStats.m_passtimeStats.summary.nTotalPassesStarted;
  691. m_ballController.SetTargetSpeed( tf_passtime_mode_homing_speed.GetFloat() );
  692. auto isCharged = (m_fChargeBeginTime > 0) && (GetCurrentCharge() >= 1);
  693. m_ballController.StartHoming( g_pPasstimeLogic->GetBall(), pPassTarget, isCharged );
  694. if ( CTFPlayer *pPlayerPassTarget = ToTFPlayer( pPassTarget ) )
  695. {
  696. char pszSound[64];
  697. IncomingSoundForClass( pOwner->GetPlayerClass(), pszSound );
  698. {
  699. // for the thrower
  700. CRecipientFilter filter;
  701. filter.MakeReliable();
  702. filter.AddRecipient( pOwner );
  703. filter.AddRecipientsByTeam( TFTeamMgr()->GetTeam( TEAM_SPECTATOR ) );
  704. pOwner->EmitSound( filter, pOwner->entindex(), pszSound );
  705. }
  706. {
  707. // for the catcher
  708. CRecipientFilter filter;
  709. filter.MakeReliable();
  710. filter.AddRecipient( pPlayerPassTarget );
  711. pPlayerPassTarget->EmitSound( filter, pPlayerPassTarget->entindex(), pszSound );
  712. }
  713. }
  714. }
  715. else
  716. {
  717. ++CTF_GameStats.m_passtimeStats.summary.nTotalTosses;
  718. }
  719. #else
  720. pOwner->m_Shared.SetHasPasstimeBall( 0 ); // predict throwing
  721. #endif
  722. pOwner->m_Shared.SetPasstimePassTarget( 0 );
  723. }
  724. //-----------------------------------------------------------------------------
  725. void CPasstimeGun::ItemHolsterFrame()
  726. {
  727. CTFPlayer *pOwner = ToTFPlayer( GetOwner() );
  728. if ( pOwner && pOwner->m_Shared.HasPasstimeBall() )
  729. {
  730. m_hStoredLastWpn = GetTFPlayerOwner()->GetActiveWeapon();
  731. pOwner->Weapon_Switch( this );
  732. }
  733. }
  734. //-----------------------------------------------------------------------------
  735. const char *CPasstimeGun::GetWorldModel() const
  736. {
  737. if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) )
  738. {
  739. return kHalloweenBallModel;
  740. }
  741. return tf_passtime_ball_model.GetString();
  742. }
  743. //-----------------------------------------------------------------------------
  744. bool CPasstimeGun::Deploy()
  745. {
  746. // This is not called on the client because the client can't predict it.
  747. if ( !BaseClass::Deploy() )
  748. {
  749. return false;
  750. }
  751. m_eThrowState = THROWSTATE_IDLE;
  752. m_attack2.UnlatchUp();
  753. m_attack.UnlatchUp();
  754. return true;
  755. }
  756. //-----------------------------------------------------------------------------
  757. bool CPasstimeGun::CanDeploy()
  758. {
  759. CTFPlayer *pOwner = GetTFPlayerOwner();
  760. return pOwner && pOwner->m_Shared.HasPasstimeBall() && BaseClass::CanDeploy();
  761. }
  762. //-----------------------------------------------------------------------------
  763. // static
  764. bool CPasstimeGun::BValidPassTarget( CTFPlayer *pSource, CTFPlayer *pTarget, HudNotification_t *pReason )
  765. {
  766. if ( pReason ) *pReason = (HudNotification_t) 0;
  767. if ( !pTarget || (pTarget == pSource) )
  768. {
  769. return false;
  770. }
  771. bool bTargetDisguised = pTarget->m_Shared.InCond( TF_COND_DISGUISED );
  772. int iTargetTeam = pTarget->GetTeamNumber();
  773. int iSourceTeam = pSource ? pSource->GetTeamNumber() : iTargetTeam;
  774. bool bSameTeam = iTargetTeam == iSourceTeam;
  775. bool bTargetableEnemySpy = !bSameTeam && bTargetDisguised && (pTarget->m_Shared.GetDisguiseTeam() == iSourceTeam);
  776. if ( !bSameTeam && !bTargetableEnemySpy )
  777. {
  778. // can't pass to enemies
  779. return false;
  780. }
  781. else if ( bSameTeam && bTargetDisguised )
  782. {
  783. // can't pass to disguised friendly spies
  784. if ( bTargetDisguised && pReason )
  785. {
  786. *pReason = HUD_NOTIFY_PASSTIME_NO_DISGUISE;
  787. }
  788. return false;
  789. }
  790. #ifdef CLIENT_DLL
  791. return g_pPasstimeLogic->BCanPlayerPickUpBall( pTarget );
  792. #else
  793. return g_pPasstimeLogic->BCanPlayerPickUpBall( pTarget, pReason );
  794. #endif
  795. }
  796. //-----------------------------------------------------------------------------
  797. #ifdef CLIENT_DLL
  798. void CPasstimeGun::UpdateThrowArch()
  799. {
  800. C_TFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() );
  801. if ( !pOwner )
  802. {
  803. return;
  804. }
  805. if ( !m_pBounceReticle )
  806. {
  807. m_pBounceReticle = new C_PasstimeBounceReticle();
  808. }
  809. if ( pOwner->m_Shared.GetPasstimePassTarget() )
  810. {
  811. m_pBounceReticle->Hide();
  812. return;
  813. }
  814. const LaunchParams& launchParams = CalcLaunch( pOwner, false );
  815. // Simple euler integration.
  816. // This seems to approximate what havok does reasonably accurately as long as there's no impact.
  817. Vector vecPos = launchParams.startPos;
  818. Vector vecVel = launchParams.startVel;
  819. const int iNumSuperSamples = 8;
  820. const float flDt = 1.0f / 16.0f / iNumSuperSamples;
  821. const Vector vecGravity_dt = flDt * Vector( 0, 0, -800 );
  822. const float flDamping_dt = flDt * tf_passtime_ball_damping_scale.GetFloat();
  823. Vector vecStart, vecEnd;
  824. trace_t tr;
  825. CTraceFilterSimple traceFilter( pOwner, COLLISION_GROUP_NONE );
  826. const int iMaxTraces = 100; // is this insane?
  827. for ( int iPoint = 0; iPoint < iMaxTraces; ++iPoint )
  828. {
  829. vecStart = vecPos;
  830. for ( int iSuperSample = 0; iSuperSample < iNumSuperSamples; ++iSuperSample )
  831. {
  832. vecVel += vecGravity_dt;
  833. vecVel -= vecVel * flDamping_dt;
  834. vecPos += vecVel * flDt;
  835. }
  836. vecEnd = vecPos;
  837. UTIL_TraceHull( vecStart, vecEnd,
  838. -launchParams.traceHullSize, launchParams.traceHullSize,
  839. MASK_PLAYERSOLID, &traceFilter, &tr );
  840. if ( tr.DidHit() )
  841. {
  842. m_pBounceReticle->Show( tr.endpos, tr.plane.normal );
  843. break;
  844. // commented out code trying to guess bounce
  845. //vecVel = Lerp( tr.fraction, oldVel, vecVel ); // what vecVel was at point of impact, very roughly
  846. //vecPos = tr.endpos + tr.plane.normal; // move away from wall a bit
  847. //float speed = vecVel.NormalizeInPlace();
  848. //vecVel = -2 * vecVel.Dot( tr.plane.normal ) * tr.plane.normal + vecVel;
  849. //vecVel *= speed;
  850. }
  851. }
  852. if ( !tr.DidHit() )
  853. {
  854. m_pBounceReticle->Hide();
  855. }
  856. }
  857. #endif
  858. //-----------------------------------------------------------------------------
  859. //static
  860. CPasstimeGun::LaunchParams
  861. CPasstimeGun::LaunchParams::Default( CTFPlayer *pPlayer )
  862. {
  863. LaunchParams p;
  864. pPlayer->EyePositionAndVectors( &p.eyePos, &p.viewFwd, &p.viewRight, &p.viewUp );
  865. const float size = tf_passtime_ball_sphere_radius.GetFloat() / 3.0f;
  866. p.traceHullSize = Vector( size, size, size );
  867. p.traceHullDistance = 8;
  868. p.startPos = pPlayer->Weapon_ShootPosition();
  869. p.startDir = p.viewFwd;
  870. p.startVel = p.startDir;
  871. return p;
  872. }
  873. //-----------------------------------------------------------------------------
  874. static ConVar *s_pThrowSpeedConvars[TF_LAST_NORMAL_CLASS] = {
  875. nullptr, // TF_CLASS_UNDEFINED
  876. &tf_passtime_throwspeed_scout,
  877. &tf_passtime_throwspeed_sniper,
  878. &tf_passtime_throwspeed_soldier,
  879. &tf_passtime_throwspeed_demoman,
  880. &tf_passtime_throwspeed_medic,
  881. &tf_passtime_throwspeed_heavy,
  882. &tf_passtime_throwspeed_pyro,
  883. &tf_passtime_throwspeed_spy,
  884. &tf_passtime_throwspeed_engineer,
  885. };
  886. //-----------------------------------------------------------------------------
  887. static ConVar *s_pThrowArcConvars[TF_LAST_NORMAL_CLASS] = {
  888. nullptr, // TF_CLASS_UNDEFINED
  889. &tf_passtime_throwarc_scout,
  890. &tf_passtime_throwarc_sniper,
  891. &tf_passtime_throwarc_soldier,
  892. &tf_passtime_throwarc_demoman,
  893. &tf_passtime_throwarc_medic,
  894. &tf_passtime_throwarc_heavy,
  895. &tf_passtime_throwarc_pyro,
  896. &tf_passtime_throwarc_spy,
  897. &tf_passtime_throwarc_engineer,
  898. };
  899. //-----------------------------------------------------------------------------
  900. static void GetThrowParams( CTFPlayer *pPlayer, float *speed, float *arc )
  901. {
  902. Assert( pPlayer && speed && arc );
  903. if ( !pPlayer ) return;
  904. auto iClass = pPlayer->GetPlayerClass()->GetClassIndex();
  905. if ( iClass <= TF_CLASS_UNDEFINED || iClass >= TF_LAST_NORMAL_CLASS )
  906. {
  907. if ( speed ) *speed = 1000.0f;
  908. if ( arc ) *arc = 0.3f;
  909. }
  910. else
  911. {
  912. if ( speed ) *speed = s_pThrowSpeedConvars[iClass]->GetFloat();
  913. if ( arc ) *arc = s_pThrowArcConvars[iClass]->GetFloat();
  914. }
  915. }
  916. //-----------------------------------------------------------------------------
  917. // static
  918. CPasstimeGun::LaunchParams CPasstimeGun::CalcLaunch( CTFPlayer *pPlayer, bool bHoming )
  919. {
  920. auto params = LaunchParams::Default( pPlayer );
  921. params.startPos = params.eyePos;
  922. if ( !bHoming )
  923. {
  924. float speed, arc;
  925. GetThrowParams( pPlayer, &speed, &arc );
  926. params.startVel = VectorLerp( params.startDir, Vector(0,0,1), arc );
  927. params.startVel.NormalizeInPlace();
  928. params.startVel *= speed;
  929. }
  930. else if ( !tf_passtime_experiment_autopass.GetBool() )
  931. {
  932. params.startVel = params.startDir * tf_passtime_mode_homing_speed.GetFloat();
  933. }
  934. else
  935. {
  936. params.startVel = Vector(0,0,0);
  937. }
  938. // mix in some amount of forward velocity
  939. auto fwdspeed = tf_passtime_throwspeed_velocity_scale.GetFloat()
  940. * params.viewFwd.Dot( pPlayer->GetAbsVelocity() );
  941. VectorMAInline( params.startVel, fwdspeed, params.viewFwd, params.startVel );
  942. return params;
  943. }
  944. #ifdef CLIENT_DLL
  945. //-----------------------------------------------------------------------------
  946. void CPasstimeGun::ClientThink()
  947. {
  948. if ( !IsActiveByLocalPlayer() && !IsLocalPlayerSpectator() )
  949. {
  950. if ( m_pBounceReticle )
  951. {
  952. m_pBounceReticle->Hide();
  953. }
  954. return;
  955. }
  956. // doing this in ItemPostFrame makes the position jittery for some reason,
  957. // and doing it in ClientThink works better. Not entirely sure why, but I
  958. // assume it's something to do with order of operations, or possibly prediction.
  959. if ( !IsLocalPlayerSpectator() && ((m_eThrowState == THROWSTATE_CHARGING) || (m_eThrowState == THROWSTATE_CHARGED)) )
  960. {
  961. UpdateThrowArch();
  962. }
  963. else if ( (IsLocalPlayerSpectator() || (m_eThrowState != THROWSTATE_THROWN)) && m_pBounceReticle )
  964. {
  965. m_pBounceReticle->Hide();
  966. }
  967. }
  968. #endif