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.

2312 lines
70 KiB

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose:
  4. //
  5. // $NoKeywords: $
  6. //=============================================================================//
  7. #include "cbase.h"
  8. #include "tf_passtime_logic.h"
  9. #include "countdown_announcer.h"
  10. #include "entity_passtime_ball_spawn.h"
  11. #include "func_passtime_goal.h"
  12. #include "func_passtime_no_ball_zone.h"
  13. #include "tf_passtime_ball.h"
  14. #include "passtime_ballcontroller.h"
  15. #include "passtime_convars.h"
  16. #include "passtime_game_events.h"
  17. #include "func_passtime_goalie_zone.h"
  18. #include "tf_player.h"
  19. #include "tf_team.h"
  20. #include "tf_gamestats.h"
  21. #include "tf_gamerules.h"
  22. #include "pathtrack.h"
  23. #include "tf_fx.h"
  24. #include "tf_weapon_passtime_gun.h"
  25. #include "team_objectiveresource.h"
  26. #include "mapentities.h"
  27. #include "soundenvelope.h"
  28. #include "eventqueue.h"
  29. #include "hl2orange.spa.h" // achievement defines from tf_shareddefs depend on this
  30. #include "tier0/memdbgon.h"
  31. CTFPasstimeLogic *g_pPasstimeLogic;
  32. #ifdef _DEBUG
  33. #define SECRETROOM_LOG Warning
  34. #else
  35. #define SECRETROOM_LOG (void)
  36. #endif
  37. //-----------------------------------------------------------------------------
  38. LINK_ENTITY_TO_CLASS( passtime_logic, CTFPasstimeLogic );
  39. PRECACHE_REGISTER( passtime_logic );
  40. IMPLEMENT_SERVERCLASS_ST( CTFPasstimeLogic, DT_TFPasstimeLogic )
  41. SendPropEHandle( SENDINFO( m_hBall ) ),
  42. SendPropArray( SendPropVector( SENDINFO_ARRAY( m_trackPoints ), -1, SPROP_COORD_MP_INTEGRAL ), m_trackPoints ),
  43. SendPropInt( SENDINFO( m_iNumSections ) ),
  44. SendPropInt( SENDINFO( m_iCurrentSection ) ),
  45. SendPropFloat( SENDINFO( m_flMaxPassRange ) ),
  46. SendPropInt( SENDINFO( m_iBallPower ), 8 ),
  47. SendPropFloat( SENDINFO( m_flPackSpeed ) ),
  48. SendPropArray3( SENDINFO_ARRAY3( m_bPlayerIsPackMember ), SendPropInt( SENDINFO_ARRAY( m_bPlayerIsPackMember ), 1, SPROP_UNSIGNED ) ),
  49. END_SEND_TABLE()
  50. //-----------------------------------------------------------------------------
  51. BEGIN_DATADESC( CTFPasstimeLogic )
  52. DEFINE_KEYFIELD( m_iNumSections, FIELD_INTEGER, "num_sections" ),
  53. DEFINE_KEYFIELD( m_iBallSpawnCountdownSec, FIELD_INTEGER, "ball_spawn_countdown" ),
  54. DEFINE_KEYFIELD( m_flMaxPassRange, FIELD_FLOAT, "max_pass_range" ),
  55. DEFINE_INPUTFUNC( FIELD_VOID, "SpawnBall", InputSpawnBall ),
  56. DEFINE_INPUTFUNC( FIELD_STRING, "SetSection", InputSetSection ),
  57. DEFINE_INPUTFUNC( FIELD_VOID, "TimeUp", InputTimeUp ),
  58. DEFINE_INPUTFUNC( FIELD_VOID, "SpeedBoostUsed", InputSpeedBoostUsed ),
  59. DEFINE_INPUTFUNC( FIELD_VOID, "JumpPadUsed", InputJumpPadUsed ),
  60. // secret room inputs
  61. // these strings are obfuscated for fun, not for protection
  62. DEFINE_INPUTFUNC( FIELD_VOID, "statica", statica ), // SecretRoom_InputStartTouchPlayerSlot NOTE: intentionally not in FGD
  63. DEFINE_INPUTFUNC( FIELD_VOID, "staticb", staticb ), // SecretRoom_InputEndTouchPlayerSlot NOTE: intentionally not in FGD
  64. DEFINE_INPUTFUNC( FIELD_VOID, "staticc", staticc ), // SecretRoom_InputPlugDamaged NOTE: intentionally not in FGD
  65. DEFINE_INPUTFUNC( FIELD_VOID, "RoomTriggerOnTouch", InputRoomTriggerOnTouch ),
  66. DEFINE_OUTPUT( m_onBallFree, "OnBallFree" ),
  67. DEFINE_OUTPUT( m_onBallGetBlu, "OnBallGetBlu" ),
  68. DEFINE_OUTPUT( m_onBallGetRed, "OnBallGetRed" ),
  69. DEFINE_OUTPUT( m_onBallGetAny, "OnBallGetAny" ),
  70. DEFINE_OUTPUT( m_onBallRemoved, "OnBallRemoved" ),
  71. DEFINE_OUTPUT( m_onScoreBlu, "OnScoreBlu" ),
  72. DEFINE_OUTPUT( m_onScoreRed, "OnScoreRed" ),
  73. DEFINE_OUTPUT( m_onScoreAny, "OnScoreAny" ),
  74. DEFINE_OUTPUT( m_onBallPowerUp, "OnBallPowerUp" ),
  75. DEFINE_OUTPUT( m_onBallPowerDown, "OnBallPowerDown" ),
  76. END_DATADESC()
  77. //-----------------------------------------------------------------------------
  78. static const CCountdownAnnouncer::TimeSounds sCountdownSoundsRoundBegin = {
  79. "Announcer.RoundBegins60seconds",
  80. "Announcer.RoundBegins30seconds",
  81. "Announcer.RoundBegins10seconds",
  82. "Announcer.RoundBegins5seconds",
  83. "Announcer.RoundBegins4seconds",
  84. "Announcer.RoundBegins3seconds",
  85. "Announcer.RoundBegins2seconds",
  86. "Announcer.RoundBegins1seconds",
  87. };
  88. //-----------------------------------------------------------------------------
  89. static const CCountdownAnnouncer::TimeSounds sCountdownSoundsRoundBeginMerasmus = {
  90. "Announcer.RoundBegins60seconds",
  91. "Announcer.RoundBegins30seconds",
  92. "Announcer.RoundBegins10seconds",
  93. "Merasmus.RoundBegins5seconds",
  94. "Merasmus.RoundBegins4seconds",
  95. "Merasmus.RoundBegins3seconds",
  96. "Merasmus.RoundBegins2seconds",
  97. "Merasmus.RoundBegins1seconds",
  98. };
  99. //-----------------------------------------------------------------------------
  100. static bool IsGamestatePlayable()
  101. {
  102. gamerules_roundstate_t state = TFGameRules()->State_Get();
  103. return (state == GR_STATE_RND_RUNNING) || (state == GR_STATE_STALEMATE);
  104. }
  105. //-----------------------------------------------------------------------------
  106. CPasstimeBall *CTFPasstimeLogic::GetBall() const { return m_hBall; }
  107. //-----------------------------------------------------------------------------
  108. CTFPasstimeLogic::CTFPasstimeLogic()
  109. {
  110. m_SecretRoom_pTv = nullptr;
  111. m_SecretRoom_pTvSound = nullptr;
  112. m_SecretRoom_state = SecretRoomState::None;
  113. memset( m_SecretRoom_slottedPlayers, 0, sizeof( m_SecretRoom_slottedPlayers ) );
  114. m_flNextCrowdReactionTime = 0.0f;
  115. m_nPackMemberBits = 0;
  116. m_nPrevPackMemberBits = 0;
  117. }
  118. //-----------------------------------------------------------------------------
  119. CTFPasstimeLogic::~CTFPasstimeLogic()
  120. {
  121. // note:
  122. // it doesn't seem possible on the server that this destructor would be called
  123. // after a new CTFPasstimeLogic is spawned, and it's worked fine so far, but
  124. // this has been a problem in the client code.
  125. g_pPasstimeLogic = NULL;
  126. delete m_pRespawnCountdown;
  127. }
  128. //-----------------------------------------------------------------------------
  129. void CTFPasstimeLogic::Spawn()
  130. {
  131. g_pPasstimeLogic = this;
  132. m_iBallSpawnCountdownSec = MAX( 1, m_iBallSpawnCountdownSec );
  133. if ( m_flMaxPassRange == 0 )
  134. {
  135. m_flMaxPassRange = FLT_MAX;
  136. }
  137. for ( int i = 0; i < m_bPlayerIsPackMember.Count(); ++i )
  138. {
  139. m_bPlayerIsPackMember.Set( i, 0 );
  140. }
  141. for ( int i = 0; i < m_trackPoints.Count(); ++i )
  142. {
  143. m_trackPoints.GetForModify(i).Zero();
  144. }
  145. const auto *pCountdownSounds = TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween )
  146. ? &sCountdownSoundsRoundBeginMerasmus
  147. : &sCountdownSoundsRoundBegin;
  148. m_pRespawnCountdown = new CCountdownAnnouncer( pCountdownSounds );
  149. SetContextThink( &CTFPasstimeLogic::PostSpawn, gpGlobals->curtime, "postspawn" );
  150. SetContextThink( &CTFPasstimeLogic::BallPower_PackHealThink, gpGlobals->curtime + 1, "packheal" );
  151. ListenForGameEvent( "teamplay_round_stalemate" );
  152. ListenForGameEvent( "teamplay_setup_finished" );
  153. }
  154. //------------------------------------------------------------------------------
  155. // Purpose: Utility function for hooking up entity connections from code.
  156. // Would belong in BaseEnity, but this this hacky so I'm just hiding it here.
  157. //------------------------------------------------------------------------------
  158. static CBaseEntityOutput *FindOutput( CBaseEntity *pEnt, const char *pOutputName )
  159. {
  160. if ( !pEnt || !pOutputName || !pOutputName[0] )
  161. {
  162. return nullptr;
  163. }
  164. // loop taken from ValidateEntityConnections
  165. datamap_t *dmap = pEnt->GetDataDescMap();
  166. while ( dmap )
  167. {
  168. int fields = dmap->dataNumFields;
  169. for ( int i = 0; i < fields; i++ )
  170. {
  171. typedescription_t *dataDesc = &dmap->dataDesc[i];
  172. if ( ( dataDesc->fieldType == FIELD_CUSTOM )
  173. && ( dataDesc->flags & FTYPEDESC_OUTPUT )
  174. && !strcmp( dataDesc->externalName, pOutputName ) )
  175. {
  176. return (CBaseEntityOutput *)((intptr_t)pEnt + (int)dataDesc->fieldOffset[0]);
  177. }
  178. }
  179. dmap = dmap->baseMap;
  180. }
  181. return nullptr;
  182. }
  183. //------------------------------------------------------------------------------
  184. // Purpose: Utility function for hooking up entity connections from code.
  185. // Would belong in BaseEnity, but this this hacky so I'm just hiding it here.
  186. //------------------------------------------------------------------------------
  187. static void HookOutput( const char *pSourceName, string_t pTargetName,
  188. const char *pOutputName, const char *pInputName,
  189. const char *pParameter = nullptr, int nTimesToFire = EVENT_FIRE_ALWAYS )
  190. {
  191. Assert( pSourceName && pSourceName[0] );
  192. Assert( pTargetName.ToCStr() && pTargetName.ToCStr()[0] );
  193. CBaseEntity *pEnt = gEntList.FindEntityByName( nullptr, pSourceName );
  194. if ( !pEnt )
  195. {
  196. Warning( "Entity %s missing", pSourceName );
  197. return;
  198. }
  199. CBaseEntityOutput *pOut = FindOutput( pEnt, pOutputName );
  200. if ( !pOut )
  201. {
  202. Warning( "Entity %s missing output %s", pSourceName, pOutputName );
  203. return;
  204. }
  205. CEventAction *pAction = new CEventAction();
  206. pAction->m_iTarget = pTargetName;
  207. pAction->m_iTargetInput = AllocPooledString( pInputName );
  208. pAction->m_nTimesToFire = nTimesToFire;
  209. pAction->m_iParameter = pParameter
  210. ? AllocPooledString( pParameter )
  211. : string_t();
  212. pOut->AddEventAction( pAction );
  213. }
  214. //-----------------------------------------------------------------------------
  215. void CTFPasstimeLogic::PostSpawn()
  216. {
  217. // This can't be done in spawn because GetTeamNumber doesn't return the
  218. // correct value for any entity until after it's been Activate()d, which
  219. // happens after all the Spawns. And it can't be done in Activate() because
  220. // the order of Activation seems kinda non-deterministic.
  221. const auto &goals = CFuncPasstimeGoal::GetAutoList();
  222. if ( ( m_iNumSections == 0 ) && ( goals.Count() == 2 ) )
  223. {
  224. // FIXME support > 2 goals properly
  225. CFuncPasstimeGoal *pRed = static_cast<CFuncPasstimeGoal *>( goals[0] );
  226. CFuncPasstimeGoal *pBlu = static_cast<CFuncPasstimeGoal *>( goals[1] );
  227. if ( pRed->GetTeamNumber() != TF_TEAM_BLUE ) // goal's color is who can score there
  228. {
  229. V_swap( pRed, pBlu );
  230. }
  231. m_trackPoints.Set( 0, pBlu->GetAbsOrigin() );
  232. m_trackPoints.Set( 1, pRed->GetAbsOrigin() );
  233. m_iNumSections = 1;
  234. }
  235. //
  236. // Determine goal type for stats
  237. //
  238. int nTotalEndzone = 0;
  239. int nTotalBasket = 0;
  240. for ( const auto *pGoalNode : goals )
  241. {
  242. const auto *pGoal = (const CFuncPasstimeGoal *)pGoalNode;
  243. if ( !pGoal->BDisableBallScore() )
  244. {
  245. ++nTotalBasket;
  246. }
  247. if ( pGoal->BEnablePlayerScore() )
  248. {
  249. ++nTotalEndzone;
  250. }
  251. }
  252. if ( nTotalBasket && !nTotalEndzone )
  253. {
  254. CTF_GameStats.m_passtimeStats.summary.nGoalType = 1;
  255. }
  256. else if ( !nTotalBasket && nTotalEndzone )
  257. {
  258. CTF_GameStats.m_passtimeStats.summary.nGoalType = 2;
  259. }
  260. else
  261. {
  262. CTF_GameStats.m_passtimeStats.summary.nGoalType = 3;
  263. }
  264. CTF_GameStats.m_passtimeStats.summary.nRoundMaxSec = TFGameRules()->GetActiveRoundTimer()->GetTimerInitialLength();
  265. // These used to happen from teamplay_setup_ended, but that event doesn't happen if there's no setup time
  266. // These functions should be able to determine whether or not to actually do anything based on game state
  267. SetContextThink( &CTFPasstimeLogic::BallHistSampleThink, gpGlobals->curtime, "BallHistSampleThink" );
  268. SetContextThink( &CTFPasstimeLogic::OneSecStatsUpdateThink, gpGlobals->curtime, "OneSecStatsUpdateThink" );
  269. BallPower_PowerThink();
  270. BallPower_PackThink();
  271. // secret room puzzle
  272. if ( !V_stricmp( gpGlobals->mapname.ToCStr(), "pass_brickyard" ) )
  273. {
  274. SecretRoom_Spawn();
  275. }
  276. }
  277. //-----------------------------------------------------------------------------
  278. bool CTFPasstimeLogic::AddBallPower( int iPower )
  279. {
  280. int iThreshold = tf_passtime_powerball_threshold.GetInt();
  281. bool bWasAboveThreshold = m_iBallPower > iThreshold;
  282. m_iBallPower = clamp( m_iBallPower + iPower, 0, 100 );
  283. bool bIsAboveThreshold = m_iBallPower > iThreshold;
  284. if ( bWasAboveThreshold && !bIsAboveThreshold )
  285. {
  286. m_onBallPowerDown.FireOutput( this, this );
  287. TFGameRules()->BroadcastSound( 255, "Powerup.Reflect.Reflect" );
  288. return true;
  289. }
  290. else if ( !bWasAboveThreshold && bIsAboveThreshold )
  291. {
  292. m_onBallPowerUp.FireOutput( this, this );
  293. TFGameRules()->BroadcastSound( 255, "Powerup.Volume.Use" );
  294. // reschedule think so that decay stops for a while
  295. SetContextThink( &CTFPasstimeLogic::BallPower_PowerThink,
  296. gpGlobals->curtime + tf_passtime_powerball_decay_delay.GetFloat(),
  297. "BallPower_PowerThink" );
  298. return true;
  299. }
  300. return false;
  301. }
  302. //-----------------------------------------------------------------------------
  303. void CTFPasstimeLogic::ClearBallPower()
  304. {
  305. AddBallPower( -m_iBallPower );
  306. }
  307. //-----------------------------------------------------------------------------
  308. void CTFPasstimeLogic::BallPower_PowerThink()
  309. {
  310. CPasstimeBall *pBall = GetBall();
  311. if ( !IsGamestatePlayable() || !pBall )
  312. {
  313. SetContextThink( &CTFPasstimeLogic::BallPower_PowerThink,
  314. gpGlobals->curtime, "BallPower_PowerThink" );
  315. return;
  316. }
  317. float flTickTime = (pBall->GetTeamNumber() == TEAM_UNASSIGNED)
  318. ? tf_passtime_powerball_decaysec_neutral.GetFloat()
  319. : tf_passtime_powerball_decaysec.GetFloat();
  320. SetContextThink( &CTFPasstimeLogic::BallPower_PowerThink,
  321. gpGlobals->curtime + flTickTime, "BallPower_PowerThink" );
  322. if ( !pBall->GetHomingTarget() )
  323. {
  324. AddBallPower( -tf_passtime_powerball_decayamount.GetInt() );
  325. }
  326. }
  327. //-----------------------------------------------------------------------------
  328. static uint64 CalcPackMemberBits( CTFPlayer *pBallCarrier )
  329. {
  330. if ( !pBallCarrier || !pBallCarrier->IsAlive() )
  331. {
  332. return 0;
  333. }
  334. float flPackRangeSqr = tf_passtime_pack_range.GetFloat();
  335. flPackRangeSqr *= flPackRangeSqr;
  336. int iCarrierTeam = pBallCarrier->GetTeamNumber();
  337. Vector vecCarrierPos = pBallCarrier->GetAbsOrigin();
  338. uint64 nNewPackMemberBits = 0;
  339. uint64 nMask = 1;
  340. for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 )
  341. {
  342. CTFPlayer *pPlayer = (CTFPlayer*) UTIL_PlayerByIndex( i );
  343. // must be a valid team member within range
  344. if ( !pPlayer
  345. || !pPlayer->IsAlive()
  346. || ( pPlayer->GetTeamNumber() != iCarrierTeam )
  347. || ( pPlayer->GetAbsOrigin().DistToSqr( vecCarrierPos ) > flPackRangeSqr ) )
  348. {
  349. continue;
  350. }
  351. // must not be aiming (heavy spin, sniper scope, etc)
  352. if ( pPlayer->m_Shared.InCond( TF_COND_AIMING ) )
  353. {
  354. continue;
  355. }
  356. nNewPackMemberBits |= nMask;
  357. }
  358. return nNewPackMemberBits;
  359. }
  360. //-----------------------------------------------------------------------------
  361. static void SetSpeedOnFlaggedPlayers( uint64 playerBits )
  362. {
  363. if ( playerBits == 0 )
  364. {
  365. return;
  366. }
  367. uint64 nMask = 1;
  368. for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 )
  369. {
  370. if ( playerBits & nMask )
  371. {
  372. CTFPlayer *pPlayer = (CTFPlayer*)UTIL_PlayerByIndex( i );
  373. if ( pPlayer && pPlayer->IsAlive() )
  374. {
  375. pPlayer->TeamFortress_SetSpeed();
  376. }
  377. }
  378. }
  379. }
  380. //-----------------------------------------------------------------------------
  381. void CTFPasstimeLogic::ReplicatePackMemberBits()
  382. {
  383. uint64 nMask = 1;
  384. for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 )
  385. {
  386. m_bPlayerIsPackMember.Set( i, ( m_nPackMemberBits & nMask ) ? 1 : 0 );
  387. }
  388. }
  389. //-----------------------------------------------------------------------------
  390. // solo carrier is marked for death
  391. // any close teammate (2x cart distance) removes marked for death
  392. // close teammates are sped up to fastest teammate speed
  393. void CTFPasstimeLogic::BallPower_PackThink()
  394. {
  395. SetContextThink( &CTFPasstimeLogic::BallPower_PackThink,
  396. gpGlobals->curtime, "BallPower_PackThink" );
  397. m_flPackSpeed = 0.0f;
  398. m_nPrevPackMemberBits = m_nPackMemberBits;
  399. m_nPackMemberBits = 0;
  400. CTFPlayer *pCarrier = GetBallCarrier();
  401. // Check if pack speed is active
  402. if ( !tf_passtime_pack_speed.GetBool() || !IsGamestatePlayable() || !pCarrier )
  403. {
  404. m_nPackMemberBits = 0; // redundant assignment for clarity
  405. ReplicatePackMemberBits();
  406. SetSpeedOnFlaggedPlayers( m_nPrevPackMemberBits );
  407. return;
  408. }
  409. // Find the pack members
  410. m_nPackMemberBits = CalcPackMemberBits( pCarrier );
  411. ReplicatePackMemberBits();
  412. // Find the maximum MaxSpeed of the pack
  413. bool bHasNearbyTeammate = false;
  414. float flMaxMaxSpeed = -1;
  415. uint64 nMask = 1;
  416. for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 )
  417. {
  418. if ( m_nPackMemberBits & nMask )
  419. {
  420. CTFPlayer *pPlayer = (CTFPlayer*)UTIL_PlayerByIndex( i );
  421. if ( pPlayer && pPlayer->IsAlive() )
  422. {
  423. bHasNearbyTeammate = bHasNearbyTeammate || ( pPlayer != pCarrier );
  424. flMaxMaxSpeed = MAX( flMaxMaxSpeed, pPlayer->TeamFortress_CalculateMaxSpeed() );
  425. }
  426. }
  427. }
  428. // Apply marked for death if no teammates
  429. if ( !bHasNearbyTeammate )
  430. {
  431. pCarrier->m_Shared.AddCond( TF_COND_PASSTIME_PENALTY_DEBUFF, TICK_INTERVAL * 2 );
  432. }
  433. else
  434. {
  435. m_flPackSpeed = flMaxMaxSpeed;
  436. }
  437. // Now tell all the relevant players to refresh their maxspeed
  438. SetSpeedOnFlaggedPlayers( m_nPackMemberBits | m_nPrevPackMemberBits );
  439. }
  440. //-----------------------------------------------------------------------------
  441. void CTFPasstimeLogic::BallPower_PackHealThink()
  442. {
  443. SetContextThink( &CTFPasstimeLogic::BallPower_PackHealThink, gpGlobals->curtime + 1, "packheal" );
  444. CTFPlayer *pCarrier = GetBallCarrier();
  445. if ( !pCarrier )
  446. {
  447. return;
  448. }
  449. uint64 nMask = 1;
  450. uint64 nPackMemberBits = m_nPackMemberBits;
  451. float flHealAmount = tf_passtime_pack_hp_per_sec.GetFloat();
  452. for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 )
  453. {
  454. if ( ( nPackMemberBits & nMask ) == 0 )
  455. {
  456. continue;
  457. }
  458. CTFPlayer *pPlayer = (CTFPlayer*)UTIL_PlayerByIndex( i );
  459. if ( !pPlayer || ( pPlayer == pCarrier ) || !pPlayer->IsAlive() )
  460. {
  461. continue;
  462. }
  463. int iActualHealAmount = pPlayer->TakeHealth( flHealAmount, DMG_GENERIC );
  464. if ( iActualHealAmount <= 0 )
  465. {
  466. continue;
  467. }
  468. // I'm abusing the player_healonhit event because it does the visual fx I want
  469. IGameEvent *pEvent = gameeventmanager->CreateEvent( "player_healonhit" );
  470. if ( pEvent )
  471. {
  472. pEvent->SetInt( "amount", iActualHealAmount );
  473. pEvent->SetInt( "entindex", pPlayer->entindex() );
  474. gameeventmanager->FireEvent( pEvent );
  475. }
  476. }
  477. }
  478. //-----------------------------------------------------------------------------
  479. float CTFPasstimeLogic::GetPackSpeed( CTFPlayer *pPlayer ) const
  480. {
  481. if ( pPlayer )
  482. {
  483. uint64 nMask = (uint64)1 << ( pPlayer->entindex() - 1 );
  484. if ( m_nPackMemberBits & nMask )
  485. {
  486. return m_flPackSpeed;
  487. }
  488. }
  489. return 0;
  490. }
  491. //-----------------------------------------------------------------------------
  492. void CTFPasstimeLogic::FireGameEvent( IGameEvent *pEvent )
  493. {
  494. const char *pEventName = pEvent->GetName();
  495. if ( !V_strcmp( pEventName, "teamplay_round_stalemate" ) )
  496. {
  497. // this only happens when mp_stalemate_enable is on
  498. CTF_GameStats.m_passtimeStats.summary.nRoundMaxSec =
  499. TFGameRules()->GetActiveRoundTimer()->GetTimeRemaining();
  500. RespawnBall();
  501. }
  502. else if ( !V_strcmp( pEventName, "teamplay_setup_finished" ) )
  503. {
  504. // respawn the ball even though it already exists so that it doesn't
  505. // catch any rotation from any spawn box it might be sitting in that
  506. // hasn't opened yet.
  507. SpawnBallAtRandomSpawner();
  508. }
  509. }
  510. //-----------------------------------------------------------------------------
  511. // FIXME copypasta with tf_hud_passtime.cpp
  512. // For stats
  513. float CTFPasstimeLogic::CalcProgressFrac() const
  514. {
  515. if ( !GetBall() || (m_iNumSections == 0) )
  516. {
  517. return 0;
  518. }
  519. //
  520. // Which point are we trying to classify?
  521. //
  522. Vector vecOrigin;
  523. {
  524. CPasstimeBall *pBall = GetBall();
  525. CTFPlayer *pCarrier = pBall->GetCarrier();
  526. vecOrigin = pCarrier
  527. ? pCarrier->GetAbsOrigin()
  528. : pBall->GetAbsOrigin();
  529. }
  530. //
  531. // Find distance along track from first goal to last goal
  532. //
  533. float flBestLen = 0;
  534. float flTotalLen = 1; // don't set 0 so div by zero is impossible
  535. {
  536. float flBestDist = FLT_MAX;
  537. Vector vecThisPoint;
  538. Vector vecPointOnLine;
  539. Vector vecPrevPoint = m_trackPoints[0];
  540. float flThisFrac = 0;
  541. float flThisLen = 0;
  542. float flThisDist = 0;
  543. for ( int i = 1; i < 16; ++i )
  544. {
  545. vecThisPoint = m_trackPoints[i];
  546. if ( vecThisPoint.IsZero() )
  547. {
  548. break;
  549. }
  550. flThisLen = (vecThisPoint - vecPrevPoint).Length();
  551. flTotalLen += flThisLen;
  552. CalcClosestPointOnLineSegment( vecOrigin, vecPrevPoint, vecThisPoint, vecPointOnLine, &flThisFrac );
  553. flThisDist = (vecPointOnLine - vecOrigin).Length();
  554. if ( flThisDist < flBestDist )
  555. {
  556. flBestDist = flThisDist;
  557. flBestLen = flTotalLen - (flThisLen * (1.0f - flThisFrac));
  558. }
  559. vecPrevPoint = vecThisPoint;
  560. }
  561. }
  562. return (float)(flBestLen / flTotalLen);
  563. }
  564. //-----------------------------------------------------------------------------
  565. void CTFPasstimeLogic::BallHistSampleThink()
  566. {
  567. CPasstimeBall *pBall = m_hBall;
  568. if ( IsGamestatePlayable() && pBall && !pBall->BOutOfPlay() )
  569. {
  570. CTF_GameStats.m_passtimeStats.AddBallFracSample( CalcProgressFrac() );
  571. }
  572. SetContextThink( &CTFPasstimeLogic::BallHistSampleThink, gpGlobals->curtime + 0.125f, "BallHistSampleThink" );
  573. }
  574. //-----------------------------------------------------------------------------
  575. void CTFPasstimeLogic::OneSecStatsUpdateThink()
  576. {
  577. CTFTeam *pBlue = GetGlobalTFTeam( TF_TEAM_BLUE );
  578. CTFTeam *pRed = GetGlobalTFTeam( TF_TEAM_RED );
  579. // FIXME this is a hack but it'll work for now
  580. CTF_GameStats.m_passtimeStats.summary.nPlayersBlueMax = MAX( CTF_GameStats.m_passtimeStats.summary.nPlayersBlueMax, pBlue->GetNumPlayers() );
  581. CTF_GameStats.m_passtimeStats.summary.nPlayersRedMax = MAX( CTF_GameStats.m_passtimeStats.summary.nPlayersRedMax, pRed->GetNumPlayers() );
  582. SetContextThink( &CTFPasstimeLogic::OneSecStatsUpdateThink, gpGlobals->curtime + 1, "OneSecStatsUpdateThink" );
  583. }
  584. //-----------------------------------------------------------------------------
  585. static void MapEventStat( CBaseEntity *pActivator, CPasstimeBall *pBall, int *pTotal, int *pCarrierTotal )
  586. {
  587. CTFPlayer *pPlayer = ToTFPlayer( pActivator );
  588. if ( pPlayer )
  589. {
  590. ++(*pTotal);
  591. if ( pBall && (pBall->GetCarrier() == pPlayer) )
  592. {
  593. ++(*pCarrierTotal);
  594. }
  595. }
  596. }
  597. //-----------------------------------------------------------------------------
  598. CTFPlayer *CTFPasstimeLogic::GetBallCarrier() const
  599. {
  600. const CPasstimeBall *pBall = m_hBall.Get();
  601. if ( !pBall )
  602. {
  603. return nullptr;
  604. }
  605. return pBall->GetCarrier();
  606. }
  607. //-----------------------------------------------------------------------------
  608. void CTFPasstimeLogic::InputSpeedBoostUsed( inputdata_t &input )
  609. {
  610. MapEventStat( input.pActivator, GetBall(),
  611. &CTF_GameStats.m_passtimeStats.summary.nTotalSpeedBoosts,
  612. &CTF_GameStats.m_passtimeStats.summary.nTotalCarrierSpeedBoosts );
  613. }
  614. //-----------------------------------------------------------------------------
  615. void CTFPasstimeLogic::InputJumpPadUsed( inputdata_t &input )
  616. {
  617. MapEventStat( input.pActivator, GetBall(),
  618. &CTF_GameStats.m_passtimeStats.summary.nTotalJumpPads,
  619. &CTF_GameStats.m_passtimeStats.summary.nTotalCarrierJumpPads );
  620. }
  621. //-----------------------------------------------------------------------------
  622. void CTFPasstimeLogic::Precache()
  623. {
  624. PrecacheScriptSound( "Passtime.BallIntercepted" );
  625. PrecacheScriptSound( "Passtime.BallStolen" );
  626. PrecacheScriptSound( "Passtime.BallDropped" );
  627. PrecacheScriptSound( "Passtime.BallCatch" );
  628. PrecacheScriptSound( "Passtime.BallSpawn" );
  629. PrecacheScriptSound( "Passtime.Crowd.Boo" );
  630. PrecacheScriptSound( "Passtime.Crowd.Cheer" );
  631. PrecacheScriptSound( "Passtime.Crowd.React.Neg" );
  632. PrecacheScriptSound( "Passtime.Crowd.React.Pos" );
  633. PrecacheScriptSound( "Powerup.Reflect.Reflect" ); // for powerball
  634. PrecacheScriptSound( "Powerup.Volume.Use" );
  635. PrecacheScriptSound( "Announcer.RoundBegins60seconds");
  636. PrecacheScriptSound( "Announcer.RoundBegins30seconds");
  637. PrecacheScriptSound( "Announcer.RoundBegins10seconds");
  638. if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) )
  639. {
  640. PrecacheScriptSound( "Merasmus.RoundBegins5seconds");
  641. PrecacheScriptSound( "Merasmus.RoundBegins4seconds");
  642. PrecacheScriptSound( "Merasmus.RoundBegins3seconds");
  643. PrecacheScriptSound( "Merasmus.RoundBegins2seconds");
  644. PrecacheScriptSound( "Merasmus.RoundBegins1seconds");
  645. PrecacheScriptSound( "sf14.Merasmus.Soccer.GoalRed" );
  646. PrecacheScriptSound( "sf14.Merasmus.Soccer.GoalBlue" );
  647. PrecacheScriptSound( "Passtime.Merasmus.Laugh" );
  648. }
  649. else
  650. {
  651. PrecacheScriptSound( "Announcer.RoundBegins5seconds");
  652. PrecacheScriptSound( "Announcer.RoundBegins4seconds");
  653. PrecacheScriptSound( "Announcer.RoundBegins3seconds");
  654. PrecacheScriptSound( "Announcer.RoundBegins2seconds");
  655. PrecacheScriptSound( "Announcer.RoundBegins1seconds");
  656. }
  657. PrecacheScriptSound( "Game.Overtime");
  658. PrecacheScriptSound( "Passtime.AskForBall" );
  659. // secret room stuff
  660. if ( !V_stricmp( gpGlobals->mapname.ToCStr(), "pass_brickyard" ) )
  661. {
  662. PrecacheScriptSound( "Passtime.Tv1" );
  663. PrecacheScriptSound( "Passtime.Tv2" );
  664. PrecacheScriptSound( "Passtime.Tv3" );
  665. }
  666. }
  667. //-----------------------------------------------------------------------------
  668. void CTFPasstimeLogic::OnEnterGoal( CPasstimeBall *pBall, CFuncPasstimeGoal *pGoal )
  669. {
  670. if ( pGoal->BDisableBallScore() || !IsGamestatePlayable() )
  671. {
  672. return;
  673. }
  674. // -1 iPoints is a special hacked value that means "kill zone"
  675. if ( pGoal->Points() == -1 )
  676. {
  677. m_onBallRemoved.FireOutput( pGoal, pGoal );
  678. SetContextThink( &CTFPasstimeLogic::RespawnBall, gpGlobals->curtime, "spawnball" );
  679. return;
  680. }
  681. if ( (pBall->GetCollisionCount() > 0) || (pBall->GetTeamNumber() == TEAM_UNASSIGNED) )
  682. {
  683. return;
  684. }
  685. CTFPlayer *pOwner = pBall->GetThrower();
  686. if ( pOwner && (pBall->GetTeamNumber() == pGoal->GetTeamNumber()) )
  687. {
  688. Score( pBall, pGoal );
  689. }
  690. }
  691. //-----------------------------------------------------------------------------
  692. void CTFPasstimeLogic::OnEnterGoal( CTFPlayer *pPlayer, CFuncPasstimeGoal *pGoal )
  693. {
  694. if ( IsGamestatePlayable()
  695. && pGoal->BEnablePlayerScore()
  696. && pPlayer->m_Shared.HasPasstimeBall()
  697. && (pPlayer->GetTeamNumber() == pGoal->GetTeamNumber()) )
  698. {
  699. Score( pPlayer, pGoal );
  700. }
  701. }
  702. //-----------------------------------------------------------------------------
  703. void CTFPasstimeLogic::OnExitGoal( CPasstimeBall *pBall, CFuncPasstimeGoal *pGoal )
  704. {
  705. }
  706. //-----------------------------------------------------------------------------
  707. void CTFPasstimeLogic::OnStayInGoal( CTFPlayer *pPlayer, CFuncPasstimeGoal *pGoal )
  708. {
  709. OnEnterGoal( pPlayer, pGoal );
  710. }
  711. //-----------------------------------------------------------------------------
  712. bool CTFPasstimeLogic::OnBallCollision( CPasstimeBall *pBall, int index, gamevcollisionevent_t *pEvent )
  713. {
  714. if ( !IsGamestatePlayable() )
  715. {
  716. return false;
  717. }
  718. // FIXME
  719. //if ( pBall && pBall->BAnyControllerApplied() )
  720. //{
  721. // return false;
  722. //}
  723. return true;
  724. }
  725. //-----------------------------------------------------------------------------
  726. bool CTFPasstimeLogic::BCanPlayerPickUpBall( CTFPlayer *pPlayer, HudNotification_t *pReason ) const
  727. {
  728. if ( pReason ) *pReason = (HudNotification_t) 0;
  729. const auto *pBall = m_hBall.Get();
  730. if ( !pBall )
  731. {
  732. return false;
  733. }
  734. if ( !pPlayer || !IsGamestatePlayable() )
  735. {
  736. return false;
  737. }
  738. if ( pPlayer->m_Shared.InCond( TF_COND_INVULNERABLE )
  739. || pPlayer->m_Shared.InCond( TF_COND_PHASE )
  740. || pPlayer->m_Shared.InCond( TF_COND_INVULNERABLE_WEARINGOFF ) )
  741. {
  742. if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_INVULN;
  743. return false;
  744. }
  745. if ( pPlayer->m_Shared.IsStealthed() )
  746. {
  747. if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_CLOAK;
  748. return false;
  749. }
  750. // let disguised spies pick up enemy ball, which amounts to interception and fake passes
  751. auto bEnemyBall = ( pBall->GetTeamNumber() != TEAM_UNASSIGNED )
  752. && ( pBall->GetTeamNumber() != pPlayer->GetTeamNumber() );
  753. if ( pPlayer->m_Shared.InCond( TF_COND_DISGUISED ) && !bEnemyBall )
  754. {
  755. if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_DISGUISE;
  756. return false;
  757. }
  758. if ( pPlayer->m_Shared.IsCarryingObject() )
  759. {
  760. if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_CARRY;
  761. return false;
  762. }
  763. if ( pPlayer->m_Shared.InCond( TF_COND_SELECTED_TO_TELEPORT ) )
  764. {
  765. if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_TELE;
  766. return false;
  767. }
  768. if ( pPlayer->IsTaunting() )
  769. {
  770. if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_TAUNT;
  771. return false;
  772. }
  773. if ( !pPlayer->IsAllowedToPickUpFlag()
  774. || !pPlayer->IsAlive() // NOTE: it's possible to be !alive and !dead at the same time
  775. || pPlayer->IsAwayFromKeyboard()
  776. || pPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE )
  777. || pPlayer->m_Shared.IsControlStunned() )
  778. {
  779. return false;
  780. }
  781. if ( pPlayer->m_bIsTeleportingUsingEurekaEffect )
  782. {
  783. if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_TELE;
  784. return false;
  785. }
  786. CTFWeaponBase *pActiveWeapon = pPlayer->GetActiveTFWeapon();
  787. if ( pActiveWeapon )
  788. {
  789. bool bCanHolster = pActiveWeapon->CanHolster()
  790. && !( pActiveWeapon->IsReloading() && pActiveWeapon->ReloadsSingly() && pActiveWeapon->CanOverload() ); // semihack to fix beggars bazooka problems
  791. if ( pActiveWeapon && ( pActiveWeapon->GetWeaponID() != TF_WEAPON_PASSTIME_GUN ) && !bCanHolster )
  792. {
  793. if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_HOLSTER;
  794. return false;
  795. }
  796. }
  797. if ( EntityIsInNoBallZone( pPlayer ) )
  798. {
  799. if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_OOB;
  800. return false;
  801. }
  802. return true;
  803. }
  804. //-----------------------------------------------------------------------------
  805. int CTFPasstimeLogic::UpdateTransmitState()
  806. {
  807. return SetTransmitState( FL_EDICT_ALWAYS );
  808. }
  809. //-----------------------------------------------------------------------------
  810. void CTFPasstimeLogic::RespawnBall()
  811. {
  812. Assert( m_hBall );
  813. if ( !m_hBall ) // paranoia
  814. {
  815. return;
  816. }
  817. ClearBallPower();
  818. // TFGameRules only checks capture limit once per second, so this code can't rely on game state changing
  819. int iScoreLimit = tf_passtime_scores_per_round.GetInt();
  820. bool bGameOver = ( TFTeamMgr()->GetFlagCaptures( TF_TEAM_RED ) >= iScoreLimit )
  821. || ( TFTeamMgr()->GetFlagCaptures( TF_TEAM_BLUE ) >= iScoreLimit );
  822. gamerules_roundstate_t state = TFGameRules()->State_Get();
  823. if ( bGameOver || (state == GR_STATE_GAME_OVER) || (state == GR_STATE_TEAM_WIN) || (state == GR_STATE_RESTART) )
  824. {
  825. m_hBall->SetStateOutOfPlay();
  826. MoveBallToSpawner();
  827. }
  828. else if ( (state == GR_STATE_RND_RUNNING) || (state == GR_STATE_STALEMATE) )
  829. {
  830. // TODO just end the game if there's not enough time to respawn the ball
  831. m_hBall->SetStateOutOfPlay();
  832. MoveBallToSpawner();
  833. CTeamRoundTimer *pTimer = TFGameRules()->GetActiveRoundTimer();
  834. if ( !pTimer || ( pTimer->GetTimeRemaining() > m_iBallSpawnCountdownSec ) )
  835. {
  836. m_pRespawnCountdown->Start( m_iBallSpawnCountdownSec );
  837. SpawnBallAtRandomSpawnerThink();
  838. }
  839. }
  840. else // pre-round etc
  841. {
  842. SpawnBallAtRandomSpawner();
  843. }
  844. m_ballLastHeldTimes.RemoveAll();
  845. }
  846. //-----------------------------------------------------------------------------
  847. void CTFPasstimeLogic::SpawnBallAtRandomSpawnerThink()
  848. {
  849. if ( TFGameRules()->State_Get() == GR_STATE_GAME_OVER )
  850. {
  851. m_hBall->SetStateOutOfPlay();
  852. m_pRespawnCountdown->Disable();
  853. }
  854. else if ( m_pRespawnCountdown->Tick( 1 ) )
  855. {
  856. SpawnBallAtRandomSpawner();
  857. }
  858. else
  859. {
  860. SetContextThink( &CTFPasstimeLogic::SpawnBallAtRandomSpawnerThink, gpGlobals->curtime + 1, "spawnball" );
  861. }
  862. }
  863. //-----------------------------------------------------------------------------
  864. void CTFPasstimeLogic::SpawnBallAtRandomSpawner()
  865. {
  866. const auto &allSpawns = IPasstimeBallSpawnAutoList::AutoList();
  867. int i = RandomInt( 0, allSpawns.Count() - 1 );
  868. CPasstimeBallSpawn *pSpawner = static_cast< CPasstimeBallSpawn *>( allSpawns[i] );
  869. SpawnBallAtSpawner( pSpawner );
  870. }
  871. //-----------------------------------------------------------------------------
  872. void CTFPasstimeLogic::MoveBallToSpawner()
  873. {
  874. const auto &allSpawns = IPasstimeBallSpawnAutoList::AutoList();
  875. int i = RandomInt( 0, allSpawns.Count() - 1 );
  876. CPasstimeBallSpawn *pSpawner = static_cast< CPasstimeBallSpawn *>( allSpawns[i] );
  877. m_hBall->MoveTo( pSpawner->GetAbsOrigin(), Vector( 0, 0, 0 ) );
  878. }
  879. //-----------------------------------------------------------------------------
  880. void CTFPasstimeLogic::SpawnBallAtSpawner( CPasstimeBallSpawn *pSpawner )
  881. {
  882. if ( !m_hBall )
  883. {
  884. // NOTE: this is the first place where the ball is created - on first spawn
  885. m_hBall = CPasstimeBall::Create( pSpawner->GetAbsOrigin(), QAngle(0,0,0) );
  886. }
  887. StopAskForBallEffects();
  888. m_hBall->SetStateFree();
  889. m_hBall->MoveToSpawner( pSpawner->GetAbsOrigin() );
  890. m_hBall->ChangeTeam( pSpawner->GetTeamNumber() );
  891. m_onBallFree.FireOutput( m_hBall, this );
  892. pSpawner->m_onSpawnBall.FireOutput( pSpawner, pSpawner );
  893. TFGameRules()->BroadcastSound( 255, "Passtime.BallSpawn" );
  894. if ( TFGameRules()->IsHolidayActive( kHoliday_Halloween ) )
  895. {
  896. TFGameRules()->BroadcastSound( 255, "Passtime.Merasmus.Laugh" );
  897. }
  898. }
  899. //-----------------------------------------------------------------------------
  900. void CTFPasstimeLogic::StopAskForBallEffects()
  901. {
  902. for( int i = 1; i <= MAX_PLAYERS; i++ )
  903. {
  904. CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
  905. if ( pPlayer )
  906. {
  907. pPlayer->m_Shared.SetAskForBallTime( 0 );
  908. }
  909. }
  910. }
  911. //-----------------------------------------------------------------------------
  912. void CTFPasstimeLogic::OnBallCarrierMeleeHit( CTFPlayer *pPlayer, CTFPlayer *pAttacker )
  913. {
  914. // TODO refactor OnBallCarrierMeleeHit and OnBallCarrierDamaged for less copypasta
  915. if ( !pPlayer || !pAttacker || (pPlayer == pAttacker) || !TFGameRules() )
  916. {
  917. // shouldn't happen, but who knows
  918. return;
  919. }
  920. Assert( pPlayer->m_Shared.HasPasstimeBall() );
  921. if ( !pPlayer->m_Shared.HasPasstimeBall() )
  922. {
  923. return;
  924. }
  925. if ( !pPlayer->InSameTeam( pAttacker) )
  926. {
  927. // currently handled by OnBallCarrierDamaged
  928. return;
  929. }
  930. Assert( m_hBall );
  931. if( !m_hBall )
  932. {
  933. return;
  934. }
  935. bool bTooLong = (m_hBall->GetCarryDuration() > tf_passtime_teammate_steal_time.GetFloat());
  936. if ( pPlayer->m_bPasstimeBallSlippery || bTooLong )
  937. {
  938. // once a player has held the ball too long, mark them as a jerk
  939. // so they can't hoard the ball ever again
  940. StealBall( pPlayer, pAttacker );
  941. pPlayer->m_bPasstimeBallSlippery = true;
  942. }
  943. }
  944. //-----------------------------------------------------------------------------
  945. void CTFPasstimeLogic::OnBallCarrierDamaged( CTFPlayer *pPlayer, CTFPlayer *pAttacker,
  946. const CTakeDamageInfo& info )
  947. {
  948. // TODO refactor OnBallCarrierMeleeHit and OnBallCarrierDamaged for less copypasta
  949. // NOTE: it's possible that neither player has the ball if the attacker
  950. // killed the carrier, which would cause EjectBall to happen before
  951. // this call. There's no good way around it.
  952. if ( !pPlayer || !pAttacker || (pPlayer == pAttacker) || !TFGameRules() )
  953. {
  954. // happens from world damage
  955. return;
  956. }
  957. //
  958. // Only care about melee damage
  959. //
  960. // DMG_CLUB is demo charge
  961. if ( !tf_passtime_steal_on_melee.GetBool() || !(info.GetDamageType() & (DMG_MELEE | DMG_CLUB)) )
  962. {
  963. return;
  964. }
  965. Assert( m_hBall );
  966. if ( !m_hBall )
  967. {
  968. return;
  969. }
  970. if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BASEBALL )
  971. {
  972. auto launch = CPasstimeGun::CalcLaunch( pPlayer, false );
  973. LaunchBall(pPlayer, launch.startPos, launch.startVel );
  974. }
  975. else
  976. {
  977. StealBall( pPlayer, pAttacker );
  978. }
  979. }
  980. //-----------------------------------------------------------------------------
  981. void CTFPasstimeLogic::CrowdReactionSound( int iTeam )
  982. {
  983. if ( m_flNextCrowdReactionTime <= gpGlobals->curtime )
  984. {
  985. TFGameRules()->BroadcastSound( iTeam, "Passtime.Crowd.React.Pos" );
  986. TFGameRules()->BroadcastSound( GetEnemyTeam( iTeam ), "Passtime.Crowd.React.Neg" );
  987. m_flNextCrowdReactionTime = gpGlobals->curtime + 10.0f;
  988. }
  989. }
  990. //-----------------------------------------------------------------------------
  991. void CTFPasstimeLogic::StealBall( CTFPlayer *pFrom, CTFPlayer *pTo )
  992. {
  993. if ( pFrom->m_Shared.HasPasstimeBall() )
  994. {
  995. EjectBall( pFrom, pTo );
  996. }
  997. HudNotification_t cantPickUpReason;
  998. if ( BCanPlayerPickUpBall( pTo, &cantPickUpReason ) )
  999. {
  1000. if ( !pFrom->m_bPasstimeBallSlippery )
  1001. {
  1002. CTF_GameStats.Event_PlayerAwardBonusPoints( pTo, pTo, 10 );
  1003. }
  1004. TFGameRules()->BroadcastSound( 255, "Passtime.BallStolen" );
  1005. ++CTF_GameStats.m_passtimeStats.summary.nTotalSteals;
  1006. m_hBall->SetStateCarried( pTo );
  1007. OnBallGet();
  1008. pTo->m_Shared.AddCond( TF_COND_PASSTIME_INTERCEPTION, tf_passtime_speedboost_on_get_ball_time.GetFloat() );
  1009. int pointsToAward = 5;
  1010. if ( CFuncPasstimeGoalieZone::BPlayerInAny( pTo ) )
  1011. {
  1012. ++CTF_GameStats.m_passtimeStats.summary.nTotalStealsNearGoal;
  1013. pointsToAward = 10; // Extra points for last second defend.
  1014. }
  1015. if ( !pFrom->m_bPasstimeBallSlippery )
  1016. {
  1017. CTF_GameStats.Event_PlayerAwardBonusPoints( pTo, 0, pointsToAward );
  1018. }
  1019. PasstimeGameEvents::BallStolen( pFrom->entindex(), pTo->entindex() ).Fire();
  1020. CrowdReactionSound( pTo->GetTeamNumber() );
  1021. }
  1022. else if ( cantPickUpReason )
  1023. {
  1024. CSingleUserReliableRecipientFilter filter( pTo );
  1025. TFGameRules()->SendHudNotification( filter, cantPickUpReason );
  1026. }
  1027. }
  1028. //-----------------------------------------------------------------------------
  1029. float CTFPasstimeLogic::GetLastHeldTime( CTFPlayer* pPlayer )
  1030. {
  1031. float lastHeldTime = 0.0f;
  1032. for ( int i = 0; i < m_ballLastHeldTimes.Count(); i++ )
  1033. {
  1034. if ( m_ballLastHeldTimes[i].first == pPlayer )
  1035. {
  1036. lastHeldTime = m_ballLastHeldTimes[i].second;
  1037. break;
  1038. }
  1039. }
  1040. return lastHeldTime;
  1041. }
  1042. //-----------------------------------------------------------------------------
  1043. float CTFPasstimeLogic::GetLastPassTime( CTFPlayer* pPlayer )
  1044. {
  1045. float lastPassTime = 0.0f;
  1046. for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ )
  1047. {
  1048. if ( m_ballLastPassTimes[i].first == pPlayer )
  1049. {
  1050. lastPassTime = m_ballLastPassTimes[i].second;
  1051. break;
  1052. }
  1053. }
  1054. return lastPassTime;
  1055. }
  1056. //-----------------------------------------------------------------------------
  1057. void CTFPasstimeLogic::SetLastPassTime( CTFPlayer* pPlayer )
  1058. {
  1059. std::pair<CTFPlayer*, float> toAdd( pPlayer, gpGlobals->realtime );
  1060. bool skipTheRest = false;
  1061. for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ )
  1062. {
  1063. if ( m_ballLastPassTimes[i].first == pPlayer )
  1064. {
  1065. m_ballLastPassTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector
  1066. skipTheRest = true;
  1067. break;
  1068. }
  1069. }
  1070. if ( !skipTheRest )
  1071. {
  1072. m_ballLastPassTimes.AddToTail( toAdd );
  1073. }
  1074. }
  1075. //-----------------------------------------------------------------------------
  1076. void CTFPasstimeLogic::EjectBall( CTFPlayer *pPlayer, CTFPlayer *pAttacker )
  1077. {
  1078. if ( !m_hBall )
  1079. {
  1080. // I'm not sure how this is possible, but if I'm recording with hltv in
  1081. // a listen server that has bots in it (which requires a hack in the bot
  1082. // concommand) and I restart the game while a bot is holding the ball...
  1083. // then m_hBall is invalid.
  1084. if ( pPlayer )
  1085. {
  1086. // This has to be true to get into this function for the case I just
  1087. // described above, and since the ball has been deleted somehow during
  1088. // the round restart, it probably isn't necessary to set this to 0
  1089. // because the player's going to be reset anyway. But I want to make
  1090. // sure it's correct.
  1091. pPlayer->m_Shared.SetHasPasstimeBall( 0 );
  1092. }
  1093. return;
  1094. }
  1095. m_hBall->SetStateFree();
  1096. m_hBall->ChangeTeam( TEAM_UNASSIGNED );
  1097. Vector vecEjectVel( 0, 0, 600 );
  1098. vecEjectVel += pPlayer->GetAbsVelocity() * 0.1f;
  1099. m_hBall->MoveTo( pPlayer->GetAbsOrigin() + Vector( 0, 0, 32 ), vecEjectVel );
  1100. if ( pPlayer != pAttacker )
  1101. {
  1102. pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_LOST_OBJECT );
  1103. pAttacker->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_TAUNTS );
  1104. }
  1105. m_onBallFree.FireOutput( m_hBall, this );
  1106. std::pair<CTFPlayer*, float> toAdd( pPlayer, gpGlobals->realtime );
  1107. for ( int i = 0; i < m_ballLastHeldTimes.Count(); i++ )
  1108. {
  1109. if ( m_ballLastHeldTimes[i].first == pPlayer )
  1110. {
  1111. m_ballLastHeldTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector
  1112. return;
  1113. }
  1114. }
  1115. m_ballLastHeldTimes.AddToTail( toAdd );
  1116. }
  1117. //-----------------------------------------------------------------------------
  1118. void CTFPasstimeLogic::LaunchBall( CTFPlayer *pPlayer, const Vector &vecPos, const Vector &vecVel )
  1119. {
  1120. StopAskForBallEffects();
  1121. m_hBall->SetStateFree();
  1122. m_hBall->MoveTo( vecPos, vecVel );
  1123. m_onBallFree.FireOutput( m_hBall, this );
  1124. std::pair<CTFPlayer*, float> toAdd( pPlayer, gpGlobals->realtime );
  1125. for ( int i = 0; i < m_ballLastHeldTimes.Count(); i++ )
  1126. {
  1127. if ( m_ballLastHeldTimes[i].first == pPlayer )
  1128. {
  1129. m_ballLastHeldTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector
  1130. return;
  1131. }
  1132. }
  1133. m_ballLastHeldTimes.AddToTail( toAdd );
  1134. }
  1135. //-----------------------------------------------------------------------------
  1136. void CTFPasstimeLogic::Score( CTFPlayer *pPlayer, CFuncPasstimeGoal *pGoal )
  1137. {
  1138. Assert( pPlayer && pGoal );
  1139. pGoal->OnScore( pPlayer->GetTeamNumber() );
  1140. Score( pPlayer, pGoal->GetTeamNumber(), pGoal->Points(), pGoal->BWinOnScore() );
  1141. }
  1142. //-----------------------------------------------------------------------------
  1143. void CTFPasstimeLogic::Score( CPasstimeBall *pBall, CFuncPasstimeGoal *pGoal )
  1144. {
  1145. Assert( pBall && pGoal );
  1146. CTFPlayer* pPlayer = pBall->GetThrower();
  1147. Assert( pPlayer );
  1148. pGoal->OnScore( pPlayer->GetTeamNumber() );
  1149. Score( pPlayer, pGoal->GetTeamNumber(), pGoal->Points(), pGoal->BWinOnScore() );
  1150. }
  1151. //-----------------------------------------------------------------------------
  1152. // static
  1153. void CTFPasstimeLogic::AddCondToTeam( ETFCond eCond, int iTeam, float flTime )
  1154. {
  1155. for ( int i = 1; i <= gpGlobals->maxClients; i++ )
  1156. {
  1157. CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
  1158. if ( pTFPlayer && (pTFPlayer->GetTeamNumber() == iTeam) && pTFPlayer->IsAlive() )
  1159. {
  1160. pTFPlayer->m_Shared.AddCond( eCond, flTime );
  1161. }
  1162. }
  1163. }
  1164. //-----------------------------------------------------------------------------
  1165. void CTFPasstimeLogic::Score( CTFPlayer *pPlayer, int iTeam, int iPoints, bool bForceWin )
  1166. {
  1167. StopAskForBallEffects();
  1168. m_pRespawnCountdown->Disable();
  1169. Assert( pPlayer );
  1170. if ( !pPlayer || ( iTeam == TEAM_UNASSIGNED ) )
  1171. {
  1172. return;
  1173. }
  1174. if ( bForceWin )
  1175. {
  1176. iPoints = MAX( 1, tf_passtime_scores_per_round.GetInt() - TFTeamMgr()->GetFlagCaptures( iTeam ) );
  1177. }
  1178. //
  1179. // Update stats
  1180. //
  1181. ++CTF_GameStats.m_passtimeStats.summary.nTotalScores;
  1182. ++CTF_GameStats.m_passtimeStats.classes[ pPlayer->GetPlayerClass()->GetClassIndex() ].nTotalScores;
  1183. CTF_GameStats.Event_PlayerCapturedPoint( pPlayer );
  1184. //
  1185. // Award player points
  1186. //
  1187. CTF_GameStats.Event_PlayerAwardBonusPoints( pPlayer, 0, 25 );
  1188. //
  1189. // Award player assist points
  1190. //
  1191. {
  1192. CTFPlayer *pAssister = nullptr;
  1193. float flAssisterTime = FLT_MAX;
  1194. for ( unsigned short i = 0; i < m_ballLastHeldTimes.Count(); i++ )
  1195. {
  1196. auto &tempPair = m_ballLastHeldTimes[i];
  1197. auto *pPossibleAssister = tempPair.first;
  1198. auto timeLastHeld = tempPair.second;
  1199. auto flSecAgo = gpGlobals->realtime - timeLastHeld;
  1200. if ( ( pPossibleAssister->GetTeamNumber() == pPlayer->GetTeamNumber() )
  1201. && ( pPossibleAssister != pPlayer )
  1202. && ( flSecAgo < 10.0f )
  1203. && ( flSecAgo < flAssisterTime ) )
  1204. {
  1205. pAssister = pPossibleAssister;
  1206. flAssisterTime = flSecAgo;
  1207. }
  1208. }
  1209. if ( pAssister )
  1210. {
  1211. CTF_GameStats.Event_PlayerAwardBonusPoints( pAssister, 0, 10 );
  1212. PasstimeGameEvents::Score( pPlayer->entindex(), pAssister->entindex(), iPoints ).Fire();
  1213. }
  1214. else
  1215. {
  1216. PasstimeGameEvents::Score( pPlayer->entindex(), iPoints ).Fire();
  1217. }
  1218. }
  1219. //
  1220. // Award team points
  1221. //
  1222. while ( iPoints-- > 0 )
  1223. {
  1224. TFTeamMgr()->IncrementFlagCaptures( iTeam );
  1225. }
  1226. //
  1227. // Award bonus conditions
  1228. //
  1229. AddCondToTeam( TF_COND_CRITBOOSTED_CTF_CAPTURE, pPlayer->GetTeamNumber(), tf_passtime_score_crit_sec.GetFloat() );
  1230. //
  1231. // Feedback
  1232. //
  1233. pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_FLAGCAPTURED );
  1234. TFGameRules()->BroadcastSound( iTeam, "Passtime.Crowd.Cheer" );
  1235. TFGameRules()->BroadcastSound( GetEnemyTeam( iTeam ), "Passtime.Crowd.Boo" );
  1236. if ( TFGameRules()->IsHolidayActive( kHoliday_Halloween ) )
  1237. {
  1238. const char* pszSound = ( iTeam == TF_TEAM_RED )
  1239. ? "sf14.Merasmus.Soccer.GoalRed"
  1240. : "sf14.Merasmus.Soccer.GoalBlue";
  1241. TFGameRules()->BroadcastSound( 255, pszSound );
  1242. }
  1243. //
  1244. // Game state management
  1245. //
  1246. ClearBallPower();
  1247. m_hBall->SetStateOutOfPlay();
  1248. MoveBallToSpawner(); // move it now instead of when it spawns to avoid lerping
  1249. //
  1250. // Finish round or respawn ball
  1251. //
  1252. CTeamRoundTimer *pRoundTimer = TFGameRules()->GetActiveRoundTimer();
  1253. if ( ( TFGameRules()->State_Get() == GR_STATE_STALEMATE ) || ( pRoundTimer && ( pRoundTimer->GetTimeRemaining() <= 0.0f ) ) )
  1254. {
  1255. EndRoundExpiredTimer();
  1256. }
  1257. else
  1258. {
  1259. SetContextThink( &CTFPasstimeLogic::RespawnBall, gpGlobals->curtime, "spawnball" );
  1260. }
  1261. //
  1262. // Fire outputs
  1263. //
  1264. m_onScoreAny.FireOutput( pPlayer, this );
  1265. if( iTeam == TF_TEAM_RED )
  1266. {
  1267. m_onScoreRed.FireOutput( pPlayer, this );
  1268. }
  1269. else if( iTeam == TF_TEAM_BLUE )
  1270. {
  1271. m_onScoreBlu.FireOutput( pPlayer, this );
  1272. }
  1273. }
  1274. //-----------------------------------------------------------------------------
  1275. void CTFPasstimeLogic::OnPlayerTouchBall( CTFPlayer *pCatcher, CPasstimeBall *pBall )
  1276. {
  1277. if ( pBall != m_hBall )
  1278. {
  1279. return;
  1280. }
  1281. const int iCatcherTeam = pCatcher->GetTeamNumber();
  1282. float flFeet = pBall->GetAirtimeDistance() / 16.0f;
  1283. auto iExperiment = (EPasstimeExperiment_Telepass) tf_passtime_experiment_telepass.GetInt();
  1284. //
  1285. // Check for pass and interception
  1286. //
  1287. CTFPlayer *pThrower = pBall->GetThrower();
  1288. if ( pThrower // ball must must have been thrown...
  1289. && (pBall->GetCollisionCount() == 0) // and not bounced...
  1290. && (pBall->GetTeamNumber() != TEAM_UNASSIGNED) // and not be neutral...
  1291. && (pCatcher != pBall->GetPrevCarrier())) // and not passed to yourself...
  1292. {
  1293. PasstimeGameEvents::PassCaught( pThrower->entindex(), pCatcher->entindex(), flFeet, pBall->GetAirtimeSec() ).Fire();
  1294. bool bAllowCheerSound = true;
  1295. int iDistanceBonus = ( int ) ( pBall->GetAirtimeSec() * tf_passtime_powerball_airtimebonus.GetFloat() );
  1296. iDistanceBonus = clamp( iDistanceBonus, 0, tf_passtime_powerball_maxairtimebonus.GetInt() );
  1297. int iPassPoints = tf_passtime_powerball_passpoints.GetInt();
  1298. AddBallPower( iPassPoints + iDistanceBonus );
  1299. bAllowCheerSound = m_iBallPower < tf_passtime_powerball_threshold.GetInt();
  1300. CPASFilter pasFilter( pCatcher->GetAbsOrigin() );
  1301. pCatcher->EmitSound( pasFilter, pCatcher->entindex(), "Passtime.BallCatch" );
  1302. // make sure this happens before BeginCarry/SEtOwner etc
  1303. if ( pThrower->GetTeamNumber() == iCatcherTeam )
  1304. {
  1305. if ( pBall->GetHomingTarget() )
  1306. {
  1307. // pass was caught by teammate
  1308. ++CTF_GameStats.m_passtimeStats.summary.nTotalPassesCompleted;
  1309. CTF_GameStats.m_passtimeStats.AddPassTravelDistSample( pBall->GetAirtimeDistance() );
  1310. // award bonus effects for pass
  1311. pCatcher->m_Shared.AddCond( TF_COND_SPEED_BOOST, tf_passtime_speedboost_on_get_ball_time.GetFloat() );
  1312. if ( CFuncPasstimeGoalieZone::BPlayerInAny( pCatcher ) )
  1313. {
  1314. ++CTF_GameStats.m_passtimeStats.summary.nTotalPassesCompletedNearGoal;
  1315. }
  1316. if ( iExperiment != EPasstimeExperiment_Telepass::None )
  1317. {
  1318. // origins need to be a copy
  1319. auto throwerOrigin = pThrower->GetAbsOrigin();
  1320. auto catcherOrigin = pCatcher->GetAbsOrigin();
  1321. CPVSFilter filterThrower( throwerOrigin );
  1322. switch( pThrower->GetTeamNumber() )
  1323. {
  1324. case TF_TEAM_RED:
  1325. TE_TFParticleEffect( filterThrower, 0.0, "teleported_red", throwerOrigin, vec3_angle );
  1326. TE_TFParticleEffect( filterThrower, 0.0, "player_sparkles_red", throwerOrigin, vec3_angle, pThrower, PATTACH_ABSORIGIN );
  1327. break;
  1328. case TF_TEAM_BLUE:
  1329. TE_TFParticleEffect( filterThrower, 0.0, "teleported_blue", throwerOrigin, vec3_angle );
  1330. TE_TFParticleEffect( filterThrower, 0.0, "player_sparkles_blue", throwerOrigin, vec3_angle, pThrower, PATTACH_ABSORIGIN );
  1331. break;
  1332. default:
  1333. break;
  1334. }
  1335. pThrower->EmitSound( "Building_Teleporter.Send" );
  1336. pCatcher->EmitSound( "Building_Teleporter.Receive" );
  1337. // then move the player
  1338. pThrower->Teleport( &catcherOrigin, nullptr, nullptr );
  1339. if ( iExperiment == EPasstimeExperiment_Telepass::SwapWithCatcher )
  1340. {
  1341. pCatcher->Teleport( &throwerOrigin, nullptr, nullptr );
  1342. CPVSFilter filterCatcher( catcherOrigin );
  1343. switch( pCatcher->GetTeamNumber() )
  1344. {
  1345. case TF_TEAM_RED:
  1346. TE_TFParticleEffect( filterCatcher, 0.0, "teleported_red", catcherOrigin, vec3_angle );
  1347. TE_TFParticleEffect( filterCatcher, 0.0, "player_sparkles_red", catcherOrigin, vec3_angle, pCatcher, PATTACH_ABSORIGIN );
  1348. break;
  1349. case TF_TEAM_BLUE:
  1350. TE_TFParticleEffect( filterCatcher, 0.0, "teleported_blue", catcherOrigin, vec3_angle );
  1351. TE_TFParticleEffect( filterCatcher, 0.0, "player_sparkles_blue", catcherOrigin, vec3_angle, pCatcher, PATTACH_ABSORIGIN );
  1352. break;
  1353. default:
  1354. break;
  1355. }
  1356. }
  1357. // then start the effects
  1358. pThrower->TeleportEffect();
  1359. }
  1360. }
  1361. else
  1362. {
  1363. // toss was caught by teammate
  1364. ++CTF_GameStats.m_passtimeStats.summary.nTotalTossesCompleted;
  1365. }
  1366. float lastPassTime = 0.0f;
  1367. for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ )
  1368. {
  1369. if ( m_ballLastPassTimes[i].first == pThrower)
  1370. {
  1371. lastPassTime = m_ballLastPassTimes[i].second;
  1372. break;
  1373. }
  1374. }
  1375. // successful pass
  1376. if ( flFeet > 30 )
  1377. {
  1378. // fanfare and points if the pass was long enough (and we haven't been spamming throw/catch for points)
  1379. if ( gpGlobals->realtime - lastPassTime > 6.0f ) // FIXME literal balance value
  1380. {
  1381. CTF_GameStats.Event_PlayerAwardBonusPoints( pThrower, pThrower, 15 ); // FIXME literal balance value
  1382. }
  1383. if ( bAllowCheerSound && ( pBall->GetAirtimeSec() > 2.0f ) ) // FIXME literal balance value
  1384. {
  1385. TFGameRules()->BroadcastSound( 255, "TFPlayer.StunImpactRange" );
  1386. }
  1387. }
  1388. else// flFeet <= 30
  1389. {
  1390. // (points conditional on we haven't had the ball in the last 6 seconds)
  1391. if ( gpGlobals->realtime - lastPassTime > 6.0f ) // FIXME literal balance value
  1392. {
  1393. CTF_GameStats.Event_PlayerAwardBonusPoints( pThrower, pThrower, 5 ); // FIXME literal balance value
  1394. }
  1395. }
  1396. std::pair<CTFPlayer*, float> toAdd( pThrower, gpGlobals->realtime );
  1397. bool skipTheRest = false;
  1398. for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ )
  1399. {
  1400. if ( m_ballLastPassTimes[i].first == pThrower)
  1401. {
  1402. m_ballLastPassTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector
  1403. skipTheRest = true;
  1404. break;
  1405. }
  1406. }
  1407. if ( !skipTheRest )
  1408. {
  1409. m_ballLastPassTimes.AddToTail( toAdd );
  1410. }
  1411. }
  1412. else
  1413. {
  1414. if ( pBall->GetHomingTarget() )
  1415. {
  1416. // pass was intercepted
  1417. ++CTF_GameStats.m_passtimeStats.summary.nTotalPassesIntercepted;
  1418. CTF_GameStats.m_passtimeStats.AddPassTravelDistSample( pBall->GetAirtimeDistance() );
  1419. }
  1420. else
  1421. {
  1422. // toss was intercepted
  1423. ++CTF_GameStats.m_passtimeStats.summary.nTotalTossesIntercepted;
  1424. }
  1425. // interception can happen at any range, extra points if intercepted within the goal area
  1426. int bonusPointsToAward = 15; // FIXME literal balance value
  1427. if ( CFuncPasstimeGoalieZone::BPlayerInAny( pCatcher ) )
  1428. {
  1429. bonusPointsToAward = 25; // FIXME literal balance value
  1430. if ( pBall->GetHomingTarget() )
  1431. {
  1432. ++CTF_GameStats.m_passtimeStats.summary.nTotalPassesInterceptedNearGoal;
  1433. }
  1434. else
  1435. {
  1436. ++CTF_GameStats.m_passtimeStats.summary.nTotalTossesInterceptedNearGoal;
  1437. }
  1438. }
  1439. // award bonus effects for interception
  1440. pCatcher->m_Shared.AddCond( TF_COND_PASSTIME_INTERCEPTION, tf_passtime_speedboost_on_get_ball_time.GetFloat() );
  1441. pCatcher->m_Shared.AddCond( TF_COND_SPEED_BOOST, tf_passtime_speedboost_on_get_ball_time.GetFloat() );
  1442. CTF_GameStats.Event_PlayerAwardBonusPoints( pCatcher, pCatcher, bonusPointsToAward );
  1443. TFGameRules()->BroadcastSound( 255, "Passtime.BallIntercepted" );
  1444. CrowdReactionSound( pCatcher->GetTeamNumber() );
  1445. }
  1446. }
  1447. else
  1448. {
  1449. ++CTF_GameStats.m_passtimeStats.summary.nTotalRecoveries;
  1450. CTFPlayer *pPrevCarrier = pBall->GetPrevCarrier();
  1451. if ( pCatcher != pPrevCarrier )
  1452. {
  1453. // Gain a point for picking up a neutral ball.
  1454. CTF_GameStats.Event_PlayerAwardBonusPoints( pCatcher, pThrower, 5 ); // FIXME literal balance value
  1455. }
  1456. PasstimeGameEvents::BallGet( pCatcher->entindex() ).Fire();
  1457. }
  1458. if ( ((iExperiment == EPasstimeExperiment_Telepass::TeleportToCatcherMaintainPossession)
  1459. || (iExperiment == EPasstimeExperiment_Telepass::SwapWithCatcher))
  1460. && BCanPlayerPickUpBall( pThrower, nullptr ) )
  1461. {
  1462. EjectBall( pCatcher, pThrower );
  1463. m_hBall->SetStateCarried( pThrower );
  1464. OnBallGet();
  1465. }
  1466. else
  1467. {
  1468. pBall->SetStateCarried( pCatcher );
  1469. OnBallGet();
  1470. }
  1471. }
  1472. //-----------------------------------------------------------------------------
  1473. void CTFPasstimeLogic::OnBallGet()
  1474. {
  1475. StopAskForBallEffects();
  1476. if ( CTFPlayer *pPlayer = m_hBall->GetCarrier() )
  1477. {
  1478. m_onBallGetAny.FireOutput( pPlayer, this );
  1479. if ( pPlayer->GetTeamNumber() == TF_TEAM_RED )
  1480. {
  1481. m_onBallGetRed.FireOutput( pPlayer, this );
  1482. }
  1483. else if ( pPlayer->GetTeamNumber() == TF_TEAM_BLUE )
  1484. {
  1485. m_onBallGetBlu.FireOutput( pPlayer, this );
  1486. }
  1487. CPasstimeBallController::BallPickedUp( m_hBall, pPlayer );
  1488. }
  1489. }
  1490. //-----------------------------------------------------------------------------
  1491. void CTFPasstimeLogic::InputSpawnBall( inputdata_t &input )
  1492. {
  1493. RespawnBall();
  1494. }
  1495. //-----------------------------------------------------------------------------
  1496. void CTFPasstimeLogic::InputTimeUp( inputdata_t &input )
  1497. {
  1498. int iRedScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_RED );
  1499. int iBlueScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_BLUE );
  1500. int iPointDifference = abs( iRedScore - iBlueScore );
  1501. // going through the list of goals to calculate the max possible point gain
  1502. // is possible but tricky since goals can be enabled/disabled and there's no
  1503. // way to know which goals are actually possible to score in, so this is
  1504. // simply hard-coded to work correctly for the official maps where there's
  1505. // a 3-point unlockable goal.
  1506. int iMaxPossibleScoreGain = 3;
  1507. if ( ( iPointDifference <= iMaxPossibleScoreGain ) && !ShouldEndOvertime() )
  1508. {
  1509. m_pRespawnCountdown->Disable();
  1510. TFGameRules()->BroadcastSound( 255, "Game.Overtime" );
  1511. ThinkExpiredTimer();
  1512. }
  1513. else
  1514. {
  1515. EndRoundExpiredTimer();
  1516. }
  1517. }
  1518. //-----------------------------------------------------------------------------
  1519. void CTFPasstimeLogic::ThinkExpiredTimer()
  1520. {
  1521. if ( TFGameRules() && (TFGameRules()->State_Get() != GR_STATE_RND_RUNNING) )
  1522. {
  1523. if ( m_pRespawnCountdown )
  1524. {
  1525. // just in case
  1526. m_pRespawnCountdown->Disable();
  1527. }
  1528. return;
  1529. }
  1530. if ( ShouldEndOvertime() || m_pRespawnCountdown->Tick( gpGlobals->frametime ) )
  1531. {
  1532. EndRoundExpiredTimer();
  1533. return;
  1534. }
  1535. // Check again every frame until either something else ends the round
  1536. // or the conditions are met that allow an expired timer to end the round.
  1537. SetContextThink( &CTFPasstimeLogic::ThinkExpiredTimer, gpGlobals->curtime, "ThinkExpiredTimer" );
  1538. Assert( m_hBall ); // verified in ShouldEndOvertime
  1539. Assert( m_pRespawnCountdown ); // always valid after Spawn
  1540. bool bBallUnassigned = m_hBall->GetTeamNumber() == TEAM_UNASSIGNED;
  1541. bool bCountdownRunning = !m_pRespawnCountdown->IsDisabled();
  1542. if ( bBallUnassigned && !bCountdownRunning )
  1543. {
  1544. // start the countdown when the ball turns neutral
  1545. m_pRespawnCountdown->Start( tf_passtime_overtime_idle_sec.GetFloat() );
  1546. }
  1547. else if ( !bBallUnassigned && bCountdownRunning )
  1548. {
  1549. // stop the countdown when the ball is picked up
  1550. m_pRespawnCountdown->Disable();
  1551. }
  1552. }
  1553. //-----------------------------------------------------------------------------
  1554. bool CTFPasstimeLogic::ShouldEndOvertime() const
  1555. {
  1556. if ( !m_hBall || !TFGameRules() )
  1557. {
  1558. return true;
  1559. }
  1560. // if nobody has the ball, only the respawn countdown can end overtime
  1561. CTFPlayer *pBallCarrier = m_hBall->GetCarrier();
  1562. if ( m_hBall->GetTeamNumber() == TEAM_UNASSIGNED || !pBallCarrier )
  1563. {
  1564. return false;
  1565. }
  1566. // if the teams are tied, someone has to score
  1567. int iRedScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_RED );
  1568. int iBluScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_BLUE );
  1569. if ( iRedScore == iBluScore )
  1570. {
  1571. return false;
  1572. }
  1573. // if the winning team has posession, they win
  1574. int iWinningTeam = ( iRedScore > iBluScore )
  1575. ? TF_TEAM_RED
  1576. : TF_TEAM_BLUE;
  1577. return pBallCarrier->GetTeamNumber() == iWinningTeam;
  1578. }
  1579. //-----------------------------------------------------------------------------
  1580. void CTFPasstimeLogic::EndRoundExpiredTimer()
  1581. {
  1582. StopAskForBallEffects();
  1583. m_pRespawnCountdown->Disable();
  1584. SetContextThink( &CTFPasstimeLogic::ThinkExpiredTimer, TICK_NEVER_THINK, "ThinkExpiredTimer" );
  1585. // copied from TeamplayRoundBasedGameRules::State_Think_RND_RUNNING
  1586. int iDrawScoreCheck = -1;
  1587. int iWinningTeam = 0;
  1588. bool bTeamsAreDrawn = true;
  1589. for ( int i = FIRST_GAME_TEAM; (i < GetNumberOfTeams()) && bTeamsAreDrawn; i++ )
  1590. {
  1591. int iTeamScore = TFTeamMgr()->GetFlagCaptures( i );
  1592. if ( iTeamScore > iDrawScoreCheck )
  1593. {
  1594. iWinningTeam = i;
  1595. }
  1596. if ( iTeamScore != iDrawScoreCheck )
  1597. {
  1598. if ( iDrawScoreCheck == -1 )
  1599. {
  1600. iDrawScoreCheck = iTeamScore;
  1601. }
  1602. else
  1603. {
  1604. bTeamsAreDrawn = false;
  1605. }
  1606. }
  1607. }
  1608. if ( bTeamsAreDrawn )
  1609. {
  1610. TFGameRules()->SetStalemate( STALEMATE_SERVER_TIMELIMIT, true );
  1611. }
  1612. else
  1613. {
  1614. TFGameRules()->SetWinningTeam( iWinningTeam, WINREASON_TIMELIMIT, true, false, false );
  1615. }
  1616. }
  1617. //-----------------------------------------------------------------------------
  1618. struct SetSectionParams
  1619. {
  1620. int num;
  1621. CPathTrack *pSectionStart;
  1622. CPathTrack *pSectionEnd;
  1623. SetSectionParams() : num(-1), pSectionStart(0), pSectionEnd(0) {}
  1624. };
  1625. //-----------------------------------------------------------------------------
  1626. bool CTFPasstimeLogic::ParseSetSection( const char *pStr, SetSectionParams &s ) const
  1627. {
  1628. char pszStartName[64];
  1629. char pszEndName[64];
  1630. const int iScanCount = sscanf( pStr, "%i %s %s", &s.num, pszStartName, pszEndName ); // WHAT YEAR IS IT
  1631. if ( iScanCount != 3 )
  1632. {
  1633. return false;
  1634. }
  1635. s.pSectionStart = dynamic_cast<CPathTrack*>( gEntList.FindEntityByName( 0, pszStartName ) );
  1636. s.pSectionEnd = dynamic_cast<CPathTrack*>( gEntList.FindEntityByName( 0, pszEndName ) );
  1637. if ( s.num < 0 )
  1638. Warning( "SetSection number (%i) must be > 0\n", s.num );
  1639. if ( s.num >= m_iNumSections )
  1640. Warning( "SetSection number (%i) must be < section count (%i)\n", s.num, m_iNumSections.Get() );
  1641. if ( !s.pSectionStart )
  1642. Warning( "Failed to find section start path_track named %s\n", pszStartName );
  1643. if ( !s.pSectionEnd)
  1644. Warning( "Failed to find section end path_track named %s\n", pszEndName );
  1645. return (s.num >= 0)
  1646. && (s.num < m_iNumSections)
  1647. && s.pSectionStart
  1648. && s.pSectionEnd;
  1649. }
  1650. //-----------------------------------------------------------------------------
  1651. void CTFPasstimeLogic::InputSetSection( inputdata_t &input )
  1652. {
  1653. SetSectionParams params;
  1654. if ( !ParseSetSection( input.value.String(), params ) )
  1655. {
  1656. Warning( "Error in SetSection input: %s\n", input.value.String() );
  1657. return;
  1658. }
  1659. for ( int i = 0; i < m_trackPoints.Count(); ++i )
  1660. {
  1661. m_trackPoints.GetForModify(i).Zero();
  1662. }
  1663. int iTrackPoint = 0;
  1664. for ( CPathTrack *pTrack = params.pSectionStart; pTrack; pTrack = pTrack->GetNext(), ++iTrackPoint )
  1665. {
  1666. if ( iTrackPoint == m_trackPoints.Count() )
  1667. {
  1668. Warning( "Too many track_path in section (%i max, easily changed but must be fixed).", m_trackPoints.Count() );
  1669. return;
  1670. }
  1671. m_trackPoints.Set( iTrackPoint, pTrack->GetAbsOrigin() );
  1672. if ( pTrack->GetAbsOrigin() == Vector( 0, 0, 0 ) )
  1673. {
  1674. // Because I'm using 0,0,0 to represent "no point" in a fixed 16-element array
  1675. Warning( "Can't have track_path at 0,0,0" );
  1676. }
  1677. if ( pTrack == params.pSectionEnd )
  1678. {
  1679. break;
  1680. }
  1681. }
  1682. m_iCurrentSection = params.num;
  1683. }
  1684. //
  1685. // Secret Room
  1686. //
  1687. //-----------------------------------------------------------------------------
  1688. void CTFPasstimeLogic::SecretRoom_Spawn()
  1689. {
  1690. SECRETROOM_LOG( "@@@@ SECRET ROOM: Spawn\n" );
  1691. m_SecretRoom_pTv = gEntList.FindEntityByName( nullptr, "tv" );
  1692. string_t self = GetEntityName();
  1693. // plug_breakable.OnDamaged -> this.InputPlugDamaged
  1694. HookOutput( "plug_breakable", self, "OnDamaged", "staticc", nullptr, 1 );
  1695. // player triggers
  1696. // the names are generated gibberish words
  1697. // (Blu) (1)Scout: "comillow"
  1698. HookOutput( "comillow", self, "OnStartTouch", "statica" );
  1699. HookOutput( "comillow", self, "OnEndTouch", "staticb" );
  1700. // (Red) (2)Soldier: "unissubs"
  1701. HookOutput( "unissubs", self, "OnStartTouch", "statica" );
  1702. HookOutput( "unissubs", self, "OnEndTouch", "staticb" );
  1703. // (Red) (3)Pyro: "amment"
  1704. HookOutput( "amment", self, "OnStartTouch", "statica" );
  1705. HookOutput( "amment", self, "OnEndTouch", "staticb" );
  1706. // (Blu) (4)Demo: "memagold"
  1707. HookOutput( "memagold", self, "OnStartTouch", "statica" );
  1708. HookOutput( "memagold", self, "OnEndTouch", "staticb" );
  1709. // (Red) (5)Heavy: "subcla"
  1710. HookOutput( "subcla", self, "OnStartTouch", "statica" );
  1711. HookOutput( "subcla", self, "OnEndTouch", "staticb" );
  1712. // (Blu) (6)Engineer: "enempose"
  1713. HookOutput( "enempose", self, "OnStartTouch", "statica" );
  1714. HookOutput( "enempose", self, "OnEndTouch", "staticb" );
  1715. // (Red) (7)Medic: "irlenous"
  1716. HookOutput( "irlenous", self, "OnStartTouch", "statica" );
  1717. HookOutput( "irlenous", self, "OnEndTouch", "staticb" );
  1718. // (Red) (8)Sniper: "donked"
  1719. HookOutput( "donked", self, "OnStartTouch", "statica" );
  1720. HookOutput( "donked", self, "OnEndTouch", "staticb" );
  1721. // (Blu) (9)Spy: "finear"
  1722. HookOutput( "finear", self, "OnStartTouch", "statica" );
  1723. HookOutput( "finear", self, "OnEndTouch", "staticb" );
  1724. // the room trigger for keeping track of who gets the achievement
  1725. HookOutput( "room_trigger", self, "OnStartTouch", "RoomTriggerOnTouch" );
  1726. g_EventQueue.AddEvent( "room_trigger", "Enable", variant_t(), 0.0f, this, this );
  1727. }
  1728. //-----------------------------------------------------------------------------
  1729. int CTFPasstimeLogic::SecretRoom_CountSlottedPlayers() const
  1730. {
  1731. int iNumSlotsFilled = 0;
  1732. for ( CTFPlayer *pPlayer : m_SecretRoom_slottedPlayers )
  1733. {
  1734. if ( pPlayer ) ++iNumSlotsFilled;
  1735. }
  1736. return iNumSlotsFilled;
  1737. }
  1738. //-----------------------------------------------------------------------------
  1739. // this doesn't need a template, but something like this in variant_t.h would
  1740. // be nice. Or maybe just some explicit overloaded constructors.
  1741. template <typename T> variant_t make_variant( T value );
  1742. template <> variant_t make_variant( int value )
  1743. {
  1744. variant_t v;
  1745. v.SetInt( value );
  1746. return v;
  1747. }
  1748. //-----------------------------------------------------------------------------
  1749. static void SecretRoom_PlayTvSound( CSoundPatch **ppPatch, int iEntIndex, const char *pSoundName, float flVolume )
  1750. {
  1751. Assert( ppPatch );
  1752. Assert( iEntIndex > 0 );
  1753. Assert( pSoundName && *pSoundName );
  1754. Assert( flVolume > 0 );
  1755. CSoundEnvelopeController &snd = CSoundEnvelopeController::GetController();
  1756. if ( *ppPatch )
  1757. {
  1758. SECRETROOM_LOG( " @@ SECRET ROOM: Destroy sound patch\n" );
  1759. snd.SoundDestroy( *ppPatch );
  1760. *ppPatch = nullptr;
  1761. }
  1762. SECRETROOM_LOG( " @@ SECRET ROOM: Create sound patch for %s volume %f\n", pSoundName, flVolume );
  1763. CReliableBroadcastRecipientFilter filter;
  1764. *ppPatch = snd.SoundCreate( filter, iEntIndex, pSoundName );
  1765. snd.Play( *ppPatch, flVolume, PITCH_NORM );
  1766. }
  1767. //-----------------------------------------------------------------------------
  1768. void CTFPasstimeLogic::SecretRoom_UpdateTv( int iNumSlotsFilled )
  1769. {
  1770. if ( iNumSlotsFilled == 9 )
  1771. {
  1772. SECRETROOM_LOG( " @@ SECRET ROOM: Update TV all slots filled\n" );
  1773. g_EventQueue.AddEvent( "screen", "Skin", make_variant( 3 ), 0.0f, this, this );
  1774. SecretRoom_PlayTvSound( &m_SecretRoom_pTvSound,
  1775. m_SecretRoom_pTv->entindex(), "Passtime.Tv3", 1.0f );
  1776. }
  1777. else
  1778. {
  1779. // sound
  1780. float volume = (float)( iNumSlotsFilled + 1 ) / 10.0f;
  1781. const char *pSoundName = ( iNumSlotsFilled >= 4 )
  1782. ? "Passtime.Tv2"
  1783. : "Passtime.Tv1";
  1784. SECRETROOM_LOG( " @@ SECRET ROOM: Update TV %i slots filled\n", iNumSlotsFilled );
  1785. SecretRoom_PlayTvSound( &m_SecretRoom_pTvSound,
  1786. m_SecretRoom_pTv->entindex(), pSoundName, volume );
  1787. // skin
  1788. int iSkin = ( iNumSlotsFilled >= 4 ) ? 2 : 1;
  1789. g_EventQueue.AddEvent( "screen", "Skin", make_variant( iSkin ), 0.0f, this, this );
  1790. }
  1791. }
  1792. //-----------------------------------------------------------------------------
  1793. struct SecretRoom_TriggerInfo
  1794. {
  1795. int iIndex;
  1796. const char *pTriggerName;
  1797. int iClass;
  1798. int iTeam;
  1799. } static const s_SecretRoom_TriggerInfo[9] =
  1800. {
  1801. { 0, "comillow", TF_CLASS_SCOUT, TF_TEAM_BLUE },
  1802. { 1, "unissubs", TF_CLASS_SOLDIER, TF_TEAM_RED },
  1803. { 2, "amment", TF_CLASS_PYRO, TF_TEAM_RED },
  1804. { 3, "memagold", TF_CLASS_DEMOMAN, TF_TEAM_BLUE },
  1805. { 4, "subcla", TF_CLASS_HEAVYWEAPONS, TF_TEAM_RED },
  1806. { 5, "enempose", TF_CLASS_ENGINEER, TF_TEAM_BLUE },
  1807. { 6, "irlenous", TF_CLASS_MEDIC, TF_TEAM_RED },
  1808. { 7, "donked", TF_CLASS_SNIPER, TF_TEAM_RED },
  1809. { 8, "finear", TF_CLASS_SPY, TF_TEAM_BLUE },
  1810. };
  1811. //-----------------------------------------------------------------------------
  1812. static const SecretRoom_TriggerInfo &SecretRoom_GetSlotInfoForTrigger(
  1813. const char *pTriggerName )
  1814. {
  1815. for ( const auto &info : s_SecretRoom_TriggerInfo )
  1816. {
  1817. if ( !V_strcmp( info.pTriggerName, pTriggerName ) )
  1818. {
  1819. return info;
  1820. }
  1821. }
  1822. Error( "Invalid trigger" );
  1823. // in case some platforms don't have noreturn attribute on Error
  1824. static SecretRoom_TriggerInfo unused;
  1825. return unused;
  1826. }
  1827. //-----------------------------------------------------------------------------
  1828. // SecretRoom_InputStartTouchPlayerSlot
  1829. void CTFPasstimeLogic::statica( inputdata_t &input )
  1830. {
  1831. SECRETROOM_LOG( "@@@@ SECRET ROOM: Start touch player slot\n" );
  1832. if ( m_SecretRoom_state != SecretRoomState::Open )
  1833. {
  1834. SECRETROOM_LOG( " @ SECRET ROOM: Ignore - state is not open\n" );
  1835. // shouldn't happen because triggers should be disabled
  1836. return;
  1837. }
  1838. if ( !input.pCaller || !input.pActivator )
  1839. {
  1840. SECRETROOM_LOG( " @ SECRET ROOM: Ignore - no caller or activator\n" );
  1841. return;
  1842. }
  1843. CTFPlayer *pActivator = ToTFPlayer( input.pActivator );
  1844. SECRETROOM_LOG( " @ SECRET ROOM: Toucher is %s\n", pActivator->GetPlayerName() );
  1845. if ( !pActivator || pActivator->IsDead() || !pActivator->IsAlive() )
  1846. {
  1847. SECRETROOM_LOG( " @ SECRET ROOM: Ignore - bad player\n" );
  1848. // not a player or not normal
  1849. return;
  1850. }
  1851. const char *pTriggerName = input.pCaller->GetEntityName().ToCStr();
  1852. const auto& info = SecretRoom_GetSlotInfoForTrigger( pTriggerName );
  1853. SECRETROOM_LOG( " @ SECRET ROOM: Trigger is %s, slot is %i\n", pTriggerName, info.iIndex );
  1854. if ( m_SecretRoom_slottedPlayers[info.iIndex] )
  1855. {
  1856. SECRETROOM_LOG( " @ SECRET ROOM: Ignore - slot already filled by %s\n", m_SecretRoom_slottedPlayers[info.iIndex]->GetPlayerName() );
  1857. // already someone filling the slot
  1858. return;
  1859. }
  1860. int iActivatorTeam = pActivator->GetTeamNumber();
  1861. int iActivatorClass = pActivator->GetPlayerClass()->GetClassIndex();
  1862. if ( !pActivator
  1863. || ( info.iTeam != iActivatorTeam )
  1864. || ( info.iClass != iActivatorClass ) )
  1865. {
  1866. SECRETROOM_LOG( " @ SECRET ROOM: Ignore - wrong class %i (%i) or team %i (%i) \n",
  1867. iActivatorTeam, info.iTeam, info.iClass, iActivatorClass );
  1868. // doesn't match
  1869. return;
  1870. }
  1871. SECRETROOM_LOG( " @ SECRET ROOM: Set slot %i to %s\n", info.iIndex, pActivator->GetPlayerName() );
  1872. // set slot
  1873. m_SecretRoom_slottedPlayers[info.iIndex] = pActivator;
  1874. // either solve puzzle or update effects
  1875. int iNumSlotsFilled = SecretRoom_CountSlottedPlayers();
  1876. SECRETROOM_LOG( " @ SECRET ROOM: %i slots filled\n", iNumSlotsFilled );
  1877. if ( iNumSlotsFilled == 9 )
  1878. {
  1879. SecretRoom_Solve();
  1880. }
  1881. else
  1882. {
  1883. SecretRoom_UpdateTv( iNumSlotsFilled );
  1884. }
  1885. }
  1886. //-----------------------------------------------------------------------------
  1887. // SecretRoom_InputEndTouchPlayerSlot
  1888. void CTFPasstimeLogic::staticb( inputdata_t &input )
  1889. {
  1890. SECRETROOM_LOG( "@@@@ SECRET ROOM: End touch player slot\n" );
  1891. if ( m_SecretRoom_state != SecretRoomState::Open )
  1892. {
  1893. SECRETROOM_LOG( " @ SECRET ROOM: Ignore - state is not open\n" );
  1894. // shouldn't happen because triggers should be disabled
  1895. return;
  1896. }
  1897. const char *pTriggerName = input.pCaller->GetEntityName().ToCStr();
  1898. const auto& info = SecretRoom_GetSlotInfoForTrigger( pTriggerName );
  1899. SECRETROOM_LOG( " @ SECRET ROOM: Trigger is %s, slot is %i\n", pTriggerName, info.iIndex );
  1900. // input.pActivator can be null if a player disconnects while inside
  1901. // the trigger. but there's no way to tell if it's the player occupying
  1902. // the slot, so clear the slot just in case
  1903. if ( input.pActivator )
  1904. {
  1905. CTFPlayer *pActivator = ToTFPlayer( input.pActivator );
  1906. SECRETROOM_LOG( " @ SECRET ROOM: Toucher is %s\n", pActivator->GetPlayerName() );
  1907. if ( !pActivator || pActivator->IsDead() || !pActivator->IsAlive() )
  1908. {
  1909. SECRETROOM_LOG( " @ SECRET ROOM: Ignore - bad player\n" );
  1910. // not a player or not normal
  1911. return;
  1912. }
  1913. if ( m_SecretRoom_slottedPlayers[info.iIndex] != input.pActivator )
  1914. {
  1915. if ( m_SecretRoom_slottedPlayers[info.iIndex] )
  1916. SECRETROOM_LOG( " @ SECRET ROOM: Ignore - slot is held by %s\n", m_SecretRoom_slottedPlayers[info.iIndex]->GetPlayerName() );
  1917. else
  1918. SECRETROOM_LOG( " @ SECRET ROOM: Ignore - slot is empty\n" );
  1919. // slot is empty already or some other player exiting the trigger
  1920. // if slot is empty: due to this code not using proper filters,
  1921. // this can be caused by players suiciding after changing teams
  1922. // while standing inside the trigger, because the suicide happens
  1923. // after the team change. this case is the entire reason for
  1924. // m_SecretRoom_slottedPlayers.
  1925. return;
  1926. }
  1927. }
  1928. // clear the slot
  1929. // note: in the case where two matching players are in the trigger
  1930. // and the one that entered first exits, the remaining player won't count
  1931. // and will have to re-enter the trigger
  1932. SECRETROOM_LOG( " @ SECRET ROOM: Clear slot %i\n", info.iIndex );
  1933. m_SecretRoom_slottedPlayers[info.iIndex] = nullptr;
  1934. // update effects
  1935. SecretRoom_UpdateTv( SecretRoom_CountSlottedPlayers() );
  1936. }
  1937. //-----------------------------------------------------------------------------
  1938. // SecretRoom_InputPlugDamaged
  1939. void CTFPasstimeLogic::staticc( inputdata_t &input )
  1940. {
  1941. SECRETROOM_LOG( "@@@@ SECRET ROOM: Plug destroyed\n" );
  1942. NOTE_UNUSED( input );
  1943. m_SecretRoom_state = SecretRoomState::Open;
  1944. // set fx for puzzle open
  1945. SecretRoom_UpdateTv( 0 );
  1946. // enable triggers
  1947. for ( const auto& info : s_SecretRoom_TriggerInfo )
  1948. {
  1949. g_EventQueue.AddEvent( info.pTriggerName, "Enable",
  1950. variant_t(), 0.0f, this, this );
  1951. }
  1952. }
  1953. //-----------------------------------------------------------------------------
  1954. void CTFPasstimeLogic::SecretRoom_Solve()
  1955. {
  1956. if ( m_SecretRoom_state != SecretRoomState::Open )
  1957. {
  1958. // paranoia
  1959. Assert( m_SecretRoom_state == SecretRoomState::Open );
  1960. return;
  1961. }
  1962. SECRETROOM_LOG( "@@@@ SECRET ROOM: Solved\n" );
  1963. m_SecretRoom_state = SecretRoomState::Solved;
  1964. // set fx for puzzle solved
  1965. g_EventQueue.AddEvent( "light", "TurnOn", variant_t(), 0.0f, this, this );
  1966. g_EventQueue.AddEvent( "spotlight", "LightOn", variant_t(), 0.0f, this, this );
  1967. g_EventQueue.AddEvent( "tv_particles", "Start", variant_t(), 0.0f, this, this );
  1968. g_EventQueue.AddEvent( "screen_image", "Enable", variant_t(), 0.0f, this, this );
  1969. SecretRoom_UpdateTv( 9 );
  1970. // disable triggers
  1971. for ( const auto& info : s_SecretRoom_TriggerInfo )
  1972. {
  1973. g_EventQueue.AddEvent( info.pTriggerName, "Disable",
  1974. variant_t(), 0.0f, this, this );
  1975. }
  1976. // achieves
  1977. for ( auto id : m_SecretRoom_playersThatTouchedRoom )
  1978. {
  1979. CTFPlayer *pPlayer = ToTFPlayer( GetPlayerBySteamID( id ) );
  1980. if ( pPlayer )
  1981. {
  1982. pPlayer->AwardAchievement( ACHIEVEMENT_TF_PASS_TIME_HAT );
  1983. }
  1984. }
  1985. m_SecretRoom_playersThatTouchedRoom.RemoveAll(); // paranoia
  1986. }
  1987. //-----------------------------------------------------------------------------
  1988. void CTFPasstimeLogic::InputRoomTriggerOnTouch( inputdata_t &input )
  1989. {
  1990. CTFPlayer *pPlayer = ToTFPlayer( input.pActivator );
  1991. if ( !pPlayer || pPlayer->IsBot() )
  1992. {
  1993. return;
  1994. }
  1995. CSteamID id;
  1996. pPlayer->GetSteamID( &id );
  1997. if ( id.IsValid() && ( m_SecretRoom_playersThatTouchedRoom.Find( id ) == -1 ) )
  1998. {
  1999. SECRETROOM_LOG( "@@@@ SECRET ROOM: Tracking %s for achievement\n", pPlayer->GetPlayerName() );
  2000. m_SecretRoom_playersThatTouchedRoom.AddToTail( id );
  2001. }
  2002. }