summaryrefslogtreecommitdiff
path: root/game/server/tf/tf_passtime_logic.cpp
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/tf/tf_passtime_logic.cpp
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'game/server/tf/tf_passtime_logic.cpp')
-rw-r--r--game/server/tf/tf_passtime_logic.cpp2312
1 files changed, 2312 insertions, 0 deletions
diff --git a/game/server/tf/tf_passtime_logic.cpp b/game/server/tf/tf_passtime_logic.cpp
new file mode 100644
index 0000000..0b50697
--- /dev/null
+++ b/game/server/tf/tf_passtime_logic.cpp
@@ -0,0 +1,2312 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+// $NoKeywords: $
+//=============================================================================//
+
+#include "cbase.h"
+#include "tf_passtime_logic.h"
+#include "countdown_announcer.h"
+#include "entity_passtime_ball_spawn.h"
+#include "func_passtime_goal.h"
+#include "func_passtime_no_ball_zone.h"
+#include "tf_passtime_ball.h"
+#include "passtime_ballcontroller.h"
+#include "passtime_convars.h"
+#include "passtime_game_events.h"
+#include "func_passtime_goalie_zone.h"
+#include "tf_player.h"
+#include "tf_team.h"
+#include "tf_gamestats.h"
+#include "tf_gamerules.h"
+#include "pathtrack.h"
+#include "tf_fx.h"
+#include "tf_weapon_passtime_gun.h"
+#include "team_objectiveresource.h"
+#include "mapentities.h"
+#include "soundenvelope.h"
+#include "eventqueue.h"
+#include "hl2orange.spa.h" // achievement defines from tf_shareddefs depend on this
+#include "tier0/memdbgon.h"
+
+CTFPasstimeLogic *g_pPasstimeLogic;
+
+#ifdef _DEBUG
+#define SECRETROOM_LOG Warning
+#else
+#define SECRETROOM_LOG (void)
+#endif
+
+//-----------------------------------------------------------------------------
+LINK_ENTITY_TO_CLASS( passtime_logic, CTFPasstimeLogic );
+PRECACHE_REGISTER( passtime_logic );
+IMPLEMENT_SERVERCLASS_ST( CTFPasstimeLogic, DT_TFPasstimeLogic )
+ SendPropEHandle( SENDINFO( m_hBall ) ),
+ SendPropArray( SendPropVector( SENDINFO_ARRAY( m_trackPoints ), -1, SPROP_COORD_MP_INTEGRAL ), m_trackPoints ),
+ SendPropInt( SENDINFO( m_iNumSections ) ),
+ SendPropInt( SENDINFO( m_iCurrentSection ) ),
+ SendPropFloat( SENDINFO( m_flMaxPassRange ) ),
+ SendPropInt( SENDINFO( m_iBallPower ), 8 ),
+ SendPropFloat( SENDINFO( m_flPackSpeed ) ),
+ SendPropArray3( SENDINFO_ARRAY3( m_bPlayerIsPackMember ), SendPropInt( SENDINFO_ARRAY( m_bPlayerIsPackMember ), 1, SPROP_UNSIGNED ) ),
+END_SEND_TABLE()
+
+//-----------------------------------------------------------------------------
+BEGIN_DATADESC( CTFPasstimeLogic )
+ DEFINE_KEYFIELD( m_iNumSections, FIELD_INTEGER, "num_sections" ),
+ DEFINE_KEYFIELD( m_iBallSpawnCountdownSec, FIELD_INTEGER, "ball_spawn_countdown" ),
+ DEFINE_KEYFIELD( m_flMaxPassRange, FIELD_FLOAT, "max_pass_range" ),
+
+ DEFINE_INPUTFUNC( FIELD_VOID, "SpawnBall", InputSpawnBall ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetSection", InputSetSection ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "TimeUp", InputTimeUp ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "SpeedBoostUsed", InputSpeedBoostUsed ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "JumpPadUsed", InputJumpPadUsed ),
+
+ // secret room inputs
+ // these strings are obfuscated for fun, not for protection
+ DEFINE_INPUTFUNC( FIELD_VOID, "statica", statica ), // SecretRoom_InputStartTouchPlayerSlot NOTE: intentionally not in FGD
+ DEFINE_INPUTFUNC( FIELD_VOID, "staticb", staticb ), // SecretRoom_InputEndTouchPlayerSlot NOTE: intentionally not in FGD
+ DEFINE_INPUTFUNC( FIELD_VOID, "staticc", staticc ), // SecretRoom_InputPlugDamaged NOTE: intentionally not in FGD
+ DEFINE_INPUTFUNC( FIELD_VOID, "RoomTriggerOnTouch", InputRoomTriggerOnTouch ),
+
+ DEFINE_OUTPUT( m_onBallFree, "OnBallFree" ),
+ DEFINE_OUTPUT( m_onBallGetBlu, "OnBallGetBlu" ),
+ DEFINE_OUTPUT( m_onBallGetRed, "OnBallGetRed" ),
+ DEFINE_OUTPUT( m_onBallGetAny, "OnBallGetAny" ),
+ DEFINE_OUTPUT( m_onBallRemoved, "OnBallRemoved" ),
+ DEFINE_OUTPUT( m_onScoreBlu, "OnScoreBlu" ),
+ DEFINE_OUTPUT( m_onScoreRed, "OnScoreRed" ),
+ DEFINE_OUTPUT( m_onScoreAny, "OnScoreAny" ),
+ DEFINE_OUTPUT( m_onBallPowerUp, "OnBallPowerUp" ),
+ DEFINE_OUTPUT( m_onBallPowerDown, "OnBallPowerDown" ),
+END_DATADESC()
+
+//-----------------------------------------------------------------------------
+static const CCountdownAnnouncer::TimeSounds sCountdownSoundsRoundBegin = {
+ "Announcer.RoundBegins60seconds",
+ "Announcer.RoundBegins30seconds",
+ "Announcer.RoundBegins10seconds",
+ "Announcer.RoundBegins5seconds",
+ "Announcer.RoundBegins4seconds",
+ "Announcer.RoundBegins3seconds",
+ "Announcer.RoundBegins2seconds",
+ "Announcer.RoundBegins1seconds",
+};
+
+//-----------------------------------------------------------------------------
+static const CCountdownAnnouncer::TimeSounds sCountdownSoundsRoundBeginMerasmus = {
+ "Announcer.RoundBegins60seconds",
+ "Announcer.RoundBegins30seconds",
+ "Announcer.RoundBegins10seconds",
+ "Merasmus.RoundBegins5seconds",
+ "Merasmus.RoundBegins4seconds",
+ "Merasmus.RoundBegins3seconds",
+ "Merasmus.RoundBegins2seconds",
+ "Merasmus.RoundBegins1seconds",
+};
+
+//-----------------------------------------------------------------------------
+static bool IsGamestatePlayable()
+{
+ gamerules_roundstate_t state = TFGameRules()->State_Get();
+ return (state == GR_STATE_RND_RUNNING) || (state == GR_STATE_STALEMATE);
+}
+
+//-----------------------------------------------------------------------------
+CPasstimeBall *CTFPasstimeLogic::GetBall() const { return m_hBall; }
+
+//-----------------------------------------------------------------------------
+CTFPasstimeLogic::CTFPasstimeLogic()
+{
+ m_SecretRoom_pTv = nullptr;
+ m_SecretRoom_pTvSound = nullptr;
+ m_SecretRoom_state = SecretRoomState::None;
+ memset( m_SecretRoom_slottedPlayers, 0, sizeof( m_SecretRoom_slottedPlayers ) );
+
+ m_flNextCrowdReactionTime = 0.0f;
+ m_nPackMemberBits = 0;
+ m_nPrevPackMemberBits = 0;
+}
+
+//-----------------------------------------------------------------------------
+CTFPasstimeLogic::~CTFPasstimeLogic()
+{
+ // note:
+ // it doesn't seem possible on the server that this destructor would be called
+ // after a new CTFPasstimeLogic is spawned, and it's worked fine so far, but
+ // this has been a problem in the client code.
+ g_pPasstimeLogic = NULL;
+ delete m_pRespawnCountdown;
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::Spawn()
+{
+ g_pPasstimeLogic = this;
+ m_iBallSpawnCountdownSec = MAX( 1, m_iBallSpawnCountdownSec );
+ if ( m_flMaxPassRange == 0 )
+ {
+ m_flMaxPassRange = FLT_MAX;
+ }
+
+ for ( int i = 0; i < m_bPlayerIsPackMember.Count(); ++i )
+ {
+ m_bPlayerIsPackMember.Set( i, 0 );
+ }
+
+ for ( int i = 0; i < m_trackPoints.Count(); ++i )
+ {
+ m_trackPoints.GetForModify(i).Zero();
+ }
+
+ const auto *pCountdownSounds = TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween )
+ ? &sCountdownSoundsRoundBeginMerasmus
+ : &sCountdownSoundsRoundBegin;
+ m_pRespawnCountdown = new CCountdownAnnouncer( pCountdownSounds );
+
+ SetContextThink( &CTFPasstimeLogic::PostSpawn, gpGlobals->curtime, "postspawn" );
+ SetContextThink( &CTFPasstimeLogic::BallPower_PackHealThink, gpGlobals->curtime + 1, "packheal" );
+
+ ListenForGameEvent( "teamplay_round_stalemate" );
+ ListenForGameEvent( "teamplay_setup_finished" );
+}
+
+//------------------------------------------------------------------------------
+// Purpose: Utility function for hooking up entity connections from code.
+// Would belong in BaseEnity, but this this hacky so I'm just hiding it here.
+//------------------------------------------------------------------------------
+static CBaseEntityOutput *FindOutput( CBaseEntity *pEnt, const char *pOutputName )
+{
+ if ( !pEnt || !pOutputName || !pOutputName[0] )
+ {
+ return nullptr;
+ }
+
+ // loop taken from ValidateEntityConnections
+ datamap_t *dmap = pEnt->GetDataDescMap();
+ while ( dmap )
+ {
+ int fields = dmap->dataNumFields;
+ for ( int i = 0; i < fields; i++ )
+ {
+ typedescription_t *dataDesc = &dmap->dataDesc[i];
+ if ( ( dataDesc->fieldType == FIELD_CUSTOM )
+ && ( dataDesc->flags & FTYPEDESC_OUTPUT )
+ && !strcmp( dataDesc->externalName, pOutputName ) )
+ {
+ return (CBaseEntityOutput *)((intptr_t)pEnt + (int)dataDesc->fieldOffset[0]);
+ }
+ }
+ dmap = dmap->baseMap;
+ }
+
+ return nullptr;
+}
+
+//------------------------------------------------------------------------------
+// Purpose: Utility function for hooking up entity connections from code.
+// Would belong in BaseEnity, but this this hacky so I'm just hiding it here.
+//------------------------------------------------------------------------------
+static void HookOutput( const char *pSourceName, string_t pTargetName,
+ const char *pOutputName, const char *pInputName,
+ const char *pParameter = nullptr, int nTimesToFire = EVENT_FIRE_ALWAYS )
+{
+ Assert( pSourceName && pSourceName[0] );
+ Assert( pTargetName.ToCStr() && pTargetName.ToCStr()[0] );
+ CBaseEntity *pEnt = gEntList.FindEntityByName( nullptr, pSourceName );
+ if ( !pEnt )
+ {
+ Warning( "Entity %s missing", pSourceName );
+ return;
+ }
+
+ CBaseEntityOutput *pOut = FindOutput( pEnt, pOutputName );
+ if ( !pOut )
+ {
+ Warning( "Entity %s missing output %s", pSourceName, pOutputName );
+ return;
+ }
+
+ CEventAction *pAction = new CEventAction();
+ pAction->m_iTarget = pTargetName;
+ pAction->m_iTargetInput = AllocPooledString( pInputName );
+ pAction->m_nTimesToFire = nTimesToFire;
+ pAction->m_iParameter = pParameter
+ ? AllocPooledString( pParameter )
+ : string_t();
+ pOut->AddEventAction( pAction );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::PostSpawn()
+{
+ // This can't be done in spawn because GetTeamNumber doesn't return the
+ // correct value for any entity until after it's been Activate()d, which
+ // happens after all the Spawns. And it can't be done in Activate() because
+ // the order of Activation seems kinda non-deterministic.
+ const auto &goals = CFuncPasstimeGoal::GetAutoList();
+
+ if ( ( m_iNumSections == 0 ) && ( goals.Count() == 2 ) )
+ {
+ // FIXME support > 2 goals properly
+ CFuncPasstimeGoal *pRed = static_cast<CFuncPasstimeGoal *>( goals[0] );
+ CFuncPasstimeGoal *pBlu = static_cast<CFuncPasstimeGoal *>( goals[1] );
+ if ( pRed->GetTeamNumber() != TF_TEAM_BLUE ) // goal's color is who can score there
+ {
+ V_swap( pRed, pBlu );
+ }
+ m_trackPoints.Set( 0, pBlu->GetAbsOrigin() );
+ m_trackPoints.Set( 1, pRed->GetAbsOrigin() );
+ m_iNumSections = 1;
+ }
+
+ //
+ // Determine goal type for stats
+ //
+ int nTotalEndzone = 0;
+ int nTotalBasket = 0;
+ for ( const auto *pGoalNode : goals )
+ {
+ const auto *pGoal = (const CFuncPasstimeGoal *)pGoalNode;
+ if ( !pGoal->BDisableBallScore() )
+ {
+ ++nTotalBasket;
+ }
+ if ( pGoal->BEnablePlayerScore() )
+ {
+ ++nTotalEndzone;
+ }
+ }
+ if ( nTotalBasket && !nTotalEndzone )
+ {
+ CTF_GameStats.m_passtimeStats.summary.nGoalType = 1;
+ }
+ else if ( !nTotalBasket && nTotalEndzone )
+ {
+ CTF_GameStats.m_passtimeStats.summary.nGoalType = 2;
+ }
+ else
+ {
+ CTF_GameStats.m_passtimeStats.summary.nGoalType = 3;
+ }
+
+ CTF_GameStats.m_passtimeStats.summary.nRoundMaxSec = TFGameRules()->GetActiveRoundTimer()->GetTimerInitialLength();
+
+ // These used to happen from teamplay_setup_ended, but that event doesn't happen if there's no setup time
+ // These functions should be able to determine whether or not to actually do anything based on game state
+ SetContextThink( &CTFPasstimeLogic::BallHistSampleThink, gpGlobals->curtime, "BallHistSampleThink" );
+ SetContextThink( &CTFPasstimeLogic::OneSecStatsUpdateThink, gpGlobals->curtime, "OneSecStatsUpdateThink" );
+
+ BallPower_PowerThink();
+ BallPower_PackThink();
+
+ // secret room puzzle
+ if ( !V_stricmp( gpGlobals->mapname.ToCStr(), "pass_brickyard" ) )
+ {
+ SecretRoom_Spawn();
+ }
+}
+
+//-----------------------------------------------------------------------------
+bool CTFPasstimeLogic::AddBallPower( int iPower )
+{
+ int iThreshold = tf_passtime_powerball_threshold.GetInt();
+ bool bWasAboveThreshold = m_iBallPower > iThreshold;
+ m_iBallPower = clamp( m_iBallPower + iPower, 0, 100 );
+ bool bIsAboveThreshold = m_iBallPower > iThreshold;
+ if ( bWasAboveThreshold && !bIsAboveThreshold )
+ {
+ m_onBallPowerDown.FireOutput( this, this );
+ TFGameRules()->BroadcastSound( 255, "Powerup.Reflect.Reflect" );
+ return true;
+ }
+ else if ( !bWasAboveThreshold && bIsAboveThreshold )
+ {
+ m_onBallPowerUp.FireOutput( this, this );
+ TFGameRules()->BroadcastSound( 255, "Powerup.Volume.Use" );
+
+ // reschedule think so that decay stops for a while
+ SetContextThink( &CTFPasstimeLogic::BallPower_PowerThink,
+ gpGlobals->curtime + tf_passtime_powerball_decay_delay.GetFloat(),
+ "BallPower_PowerThink" );
+
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::ClearBallPower()
+{
+ AddBallPower( -m_iBallPower );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::BallPower_PowerThink()
+{
+ CPasstimeBall *pBall = GetBall();
+ if ( !IsGamestatePlayable() || !pBall )
+ {
+ SetContextThink( &CTFPasstimeLogic::BallPower_PowerThink,
+ gpGlobals->curtime, "BallPower_PowerThink" );
+ return;
+ }
+
+ float flTickTime = (pBall->GetTeamNumber() == TEAM_UNASSIGNED)
+ ? tf_passtime_powerball_decaysec_neutral.GetFloat()
+ : tf_passtime_powerball_decaysec.GetFloat();
+
+ SetContextThink( &CTFPasstimeLogic::BallPower_PowerThink,
+ gpGlobals->curtime + flTickTime, "BallPower_PowerThink" );
+
+
+ if ( !pBall->GetHomingTarget() )
+ {
+ AddBallPower( -tf_passtime_powerball_decayamount.GetInt() );
+ }
+}
+
+//-----------------------------------------------------------------------------
+static uint64 CalcPackMemberBits( CTFPlayer *pBallCarrier )
+{
+ if ( !pBallCarrier || !pBallCarrier->IsAlive() )
+ {
+ return 0;
+ }
+
+ float flPackRangeSqr = tf_passtime_pack_range.GetFloat();
+ flPackRangeSqr *= flPackRangeSqr;
+ int iCarrierTeam = pBallCarrier->GetTeamNumber();
+ Vector vecCarrierPos = pBallCarrier->GetAbsOrigin();
+ uint64 nNewPackMemberBits = 0;
+ uint64 nMask = 1;
+ for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 )
+ {
+ CTFPlayer *pPlayer = (CTFPlayer*) UTIL_PlayerByIndex( i );
+
+ // must be a valid team member within range
+ if ( !pPlayer
+ || !pPlayer->IsAlive()
+ || ( pPlayer->GetTeamNumber() != iCarrierTeam )
+ || ( pPlayer->GetAbsOrigin().DistToSqr( vecCarrierPos ) > flPackRangeSqr ) )
+ {
+ continue;
+ }
+
+ // must not be aiming (heavy spin, sniper scope, etc)
+ if ( pPlayer->m_Shared.InCond( TF_COND_AIMING ) )
+ {
+ continue;
+ }
+
+ nNewPackMemberBits |= nMask;
+ }
+ return nNewPackMemberBits;
+}
+
+//-----------------------------------------------------------------------------
+static void SetSpeedOnFlaggedPlayers( uint64 playerBits )
+{
+ if ( playerBits == 0 )
+ {
+ return;
+ }
+
+ uint64 nMask = 1;
+ for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 )
+ {
+ if ( playerBits & nMask )
+ {
+ CTFPlayer *pPlayer = (CTFPlayer*)UTIL_PlayerByIndex( i );
+ if ( pPlayer && pPlayer->IsAlive() )
+ {
+ pPlayer->TeamFortress_SetSpeed();
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::ReplicatePackMemberBits()
+{
+ uint64 nMask = 1;
+ for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 )
+ {
+ m_bPlayerIsPackMember.Set( i, ( m_nPackMemberBits & nMask ) ? 1 : 0 );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// solo carrier is marked for death
+// any close teammate (2x cart distance) removes marked for death
+// close teammates are sped up to fastest teammate speed
+void CTFPasstimeLogic::BallPower_PackThink()
+{
+ SetContextThink( &CTFPasstimeLogic::BallPower_PackThink,
+ gpGlobals->curtime, "BallPower_PackThink" );
+
+ m_flPackSpeed = 0.0f;
+ m_nPrevPackMemberBits = m_nPackMemberBits;
+ m_nPackMemberBits = 0;
+
+ CTFPlayer *pCarrier = GetBallCarrier();
+
+ // Check if pack speed is active
+ if ( !tf_passtime_pack_speed.GetBool() || !IsGamestatePlayable() || !pCarrier )
+ {
+ m_nPackMemberBits = 0; // redundant assignment for clarity
+ ReplicatePackMemberBits();
+ SetSpeedOnFlaggedPlayers( m_nPrevPackMemberBits );
+ return;
+ }
+
+ // Find the pack members
+ m_nPackMemberBits = CalcPackMemberBits( pCarrier );
+ ReplicatePackMemberBits();
+
+ // Find the maximum MaxSpeed of the pack
+ bool bHasNearbyTeammate = false;
+ float flMaxMaxSpeed = -1;
+ uint64 nMask = 1;
+ for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 )
+ {
+ if ( m_nPackMemberBits & nMask )
+ {
+ CTFPlayer *pPlayer = (CTFPlayer*)UTIL_PlayerByIndex( i );
+ if ( pPlayer && pPlayer->IsAlive() )
+ {
+ bHasNearbyTeammate = bHasNearbyTeammate || ( pPlayer != pCarrier );
+ flMaxMaxSpeed = MAX( flMaxMaxSpeed, pPlayer->TeamFortress_CalculateMaxSpeed() );
+ }
+ }
+ }
+
+ // Apply marked for death if no teammates
+ if ( !bHasNearbyTeammate )
+ {
+ pCarrier->m_Shared.AddCond( TF_COND_PASSTIME_PENALTY_DEBUFF, TICK_INTERVAL * 2 );
+ }
+ else
+ {
+ m_flPackSpeed = flMaxMaxSpeed;
+ }
+
+ // Now tell all the relevant players to refresh their maxspeed
+ SetSpeedOnFlaggedPlayers( m_nPackMemberBits | m_nPrevPackMemberBits );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::BallPower_PackHealThink()
+{
+ SetContextThink( &CTFPasstimeLogic::BallPower_PackHealThink, gpGlobals->curtime + 1, "packheal" );
+
+ CTFPlayer *pCarrier = GetBallCarrier();
+ if ( !pCarrier )
+ {
+ return;
+ }
+
+ uint64 nMask = 1;
+ uint64 nPackMemberBits = m_nPackMemberBits;
+ float flHealAmount = tf_passtime_pack_hp_per_sec.GetFloat();
+ for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 )
+ {
+ if ( ( nPackMemberBits & nMask ) == 0 )
+ {
+ continue;
+ }
+
+ CTFPlayer *pPlayer = (CTFPlayer*)UTIL_PlayerByIndex( i );
+ if ( !pPlayer || ( pPlayer == pCarrier ) || !pPlayer->IsAlive() )
+ {
+ continue;
+ }
+
+ int iActualHealAmount = pPlayer->TakeHealth( flHealAmount, DMG_GENERIC );
+ if ( iActualHealAmount <= 0 )
+ {
+ continue;
+ }
+
+ // I'm abusing the player_healonhit event because it does the visual fx I want
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "player_healonhit" );
+ if ( pEvent )
+ {
+ pEvent->SetInt( "amount", iActualHealAmount );
+ pEvent->SetInt( "entindex", pPlayer->entindex() );
+ gameeventmanager->FireEvent( pEvent );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+float CTFPasstimeLogic::GetPackSpeed( CTFPlayer *pPlayer ) const
+{
+ if ( pPlayer )
+ {
+ uint64 nMask = (uint64)1 << ( pPlayer->entindex() - 1 );
+ if ( m_nPackMemberBits & nMask )
+ {
+ return m_flPackSpeed;
+ }
+ }
+ return 0;
+}
+
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::FireGameEvent( IGameEvent *pEvent )
+{
+ const char *pEventName = pEvent->GetName();
+ if ( !V_strcmp( pEventName, "teamplay_round_stalemate" ) )
+ {
+ // this only happens when mp_stalemate_enable is on
+ CTF_GameStats.m_passtimeStats.summary.nRoundMaxSec =
+ TFGameRules()->GetActiveRoundTimer()->GetTimeRemaining();
+ RespawnBall();
+ }
+ else if ( !V_strcmp( pEventName, "teamplay_setup_finished" ) )
+ {
+ // respawn the ball even though it already exists so that it doesn't
+ // catch any rotation from any spawn box it might be sitting in that
+ // hasn't opened yet.
+ SpawnBallAtRandomSpawner();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// FIXME copypasta with tf_hud_passtime.cpp
+// For stats
+float CTFPasstimeLogic::CalcProgressFrac() const
+{
+ if ( !GetBall() || (m_iNumSections == 0) )
+ {
+ return 0;
+ }
+
+ //
+ // Which point are we trying to classify?
+ //
+ Vector vecOrigin;
+ {
+ CPasstimeBall *pBall = GetBall();
+ CTFPlayer *pCarrier = pBall->GetCarrier();
+ vecOrigin = pCarrier
+ ? pCarrier->GetAbsOrigin()
+ : pBall->GetAbsOrigin();
+ }
+
+ //
+ // Find distance along track from first goal to last goal
+ //
+ float flBestLen = 0;
+ float flTotalLen = 1; // don't set 0 so div by zero is impossible
+ {
+ float flBestDist = FLT_MAX;
+
+ Vector vecThisPoint;
+ Vector vecPointOnLine;
+ Vector vecPrevPoint = m_trackPoints[0];
+ float flThisFrac = 0;
+ float flThisLen = 0;
+ float flThisDist = 0;
+ for ( int i = 1; i < 16; ++i )
+ {
+ vecThisPoint = m_trackPoints[i];
+ if ( vecThisPoint.IsZero() )
+ {
+ break;
+ }
+ flThisLen = (vecThisPoint - vecPrevPoint).Length();
+ flTotalLen += flThisLen;
+ CalcClosestPointOnLineSegment( vecOrigin, vecPrevPoint, vecThisPoint, vecPointOnLine, &flThisFrac );
+ flThisDist = (vecPointOnLine - vecOrigin).Length();
+ if ( flThisDist < flBestDist )
+ {
+ flBestDist = flThisDist;
+ flBestLen = flTotalLen - (flThisLen * (1.0f - flThisFrac));
+ }
+ vecPrevPoint = vecThisPoint;
+ }
+ }
+
+ return (float)(flBestLen / flTotalLen);
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::BallHistSampleThink()
+{
+ CPasstimeBall *pBall = m_hBall;
+ if ( IsGamestatePlayable() && pBall && !pBall->BOutOfPlay() )
+ {
+ CTF_GameStats.m_passtimeStats.AddBallFracSample( CalcProgressFrac() );
+ }
+
+ SetContextThink( &CTFPasstimeLogic::BallHistSampleThink, gpGlobals->curtime + 0.125f, "BallHistSampleThink" );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::OneSecStatsUpdateThink()
+{
+ CTFTeam *pBlue = GetGlobalTFTeam( TF_TEAM_BLUE );
+ CTFTeam *pRed = GetGlobalTFTeam( TF_TEAM_RED );
+
+ // FIXME this is a hack but it'll work for now
+ CTF_GameStats.m_passtimeStats.summary.nPlayersBlueMax = MAX( CTF_GameStats.m_passtimeStats.summary.nPlayersBlueMax, pBlue->GetNumPlayers() );
+ CTF_GameStats.m_passtimeStats.summary.nPlayersRedMax = MAX( CTF_GameStats.m_passtimeStats.summary.nPlayersRedMax, pRed->GetNumPlayers() );
+
+ SetContextThink( &CTFPasstimeLogic::OneSecStatsUpdateThink, gpGlobals->curtime + 1, "OneSecStatsUpdateThink" );
+}
+
+//-----------------------------------------------------------------------------
+static void MapEventStat( CBaseEntity *pActivator, CPasstimeBall *pBall, int *pTotal, int *pCarrierTotal )
+{
+ CTFPlayer *pPlayer = ToTFPlayer( pActivator );
+ if ( pPlayer )
+ {
+ ++(*pTotal);
+ if ( pBall && (pBall->GetCarrier() == pPlayer) )
+ {
+ ++(*pCarrierTotal);
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+CTFPlayer *CTFPasstimeLogic::GetBallCarrier() const
+{
+ const CPasstimeBall *pBall = m_hBall.Get();
+ if ( !pBall )
+ {
+ return nullptr;
+ }
+ return pBall->GetCarrier();
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::InputSpeedBoostUsed( inputdata_t &input )
+{
+ MapEventStat( input.pActivator, GetBall(),
+ &CTF_GameStats.m_passtimeStats.summary.nTotalSpeedBoosts,
+ &CTF_GameStats.m_passtimeStats.summary.nTotalCarrierSpeedBoosts );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::InputJumpPadUsed( inputdata_t &input )
+{
+ MapEventStat( input.pActivator, GetBall(),
+ &CTF_GameStats.m_passtimeStats.summary.nTotalJumpPads,
+ &CTF_GameStats.m_passtimeStats.summary.nTotalCarrierJumpPads );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::Precache()
+{
+ PrecacheScriptSound( "Passtime.BallIntercepted" );
+ PrecacheScriptSound( "Passtime.BallStolen" );
+ PrecacheScriptSound( "Passtime.BallDropped" );
+ PrecacheScriptSound( "Passtime.BallCatch" );
+ PrecacheScriptSound( "Passtime.BallSpawn" );
+
+ PrecacheScriptSound( "Passtime.Crowd.Boo" );
+ PrecacheScriptSound( "Passtime.Crowd.Cheer" );
+ PrecacheScriptSound( "Passtime.Crowd.React.Neg" );
+ PrecacheScriptSound( "Passtime.Crowd.React.Pos" );
+
+ PrecacheScriptSound( "Powerup.Reflect.Reflect" ); // for powerball
+ PrecacheScriptSound( "Powerup.Volume.Use" );
+
+ PrecacheScriptSound( "Announcer.RoundBegins60seconds");
+ PrecacheScriptSound( "Announcer.RoundBegins30seconds");
+ PrecacheScriptSound( "Announcer.RoundBegins10seconds");
+
+ if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) )
+ {
+ PrecacheScriptSound( "Merasmus.RoundBegins5seconds");
+ PrecacheScriptSound( "Merasmus.RoundBegins4seconds");
+ PrecacheScriptSound( "Merasmus.RoundBegins3seconds");
+ PrecacheScriptSound( "Merasmus.RoundBegins2seconds");
+ PrecacheScriptSound( "Merasmus.RoundBegins1seconds");
+
+ PrecacheScriptSound( "sf14.Merasmus.Soccer.GoalRed" );
+ PrecacheScriptSound( "sf14.Merasmus.Soccer.GoalBlue" );
+ PrecacheScriptSound( "Passtime.Merasmus.Laugh" );
+ }
+ else
+ {
+ PrecacheScriptSound( "Announcer.RoundBegins5seconds");
+ PrecacheScriptSound( "Announcer.RoundBegins4seconds");
+ PrecacheScriptSound( "Announcer.RoundBegins3seconds");
+ PrecacheScriptSound( "Announcer.RoundBegins2seconds");
+ PrecacheScriptSound( "Announcer.RoundBegins1seconds");
+ }
+
+ PrecacheScriptSound( "Game.Overtime");
+ PrecacheScriptSound( "Passtime.AskForBall" );
+
+ // secret room stuff
+ if ( !V_stricmp( gpGlobals->mapname.ToCStr(), "pass_brickyard" ) )
+ {
+ PrecacheScriptSound( "Passtime.Tv1" );
+ PrecacheScriptSound( "Passtime.Tv2" );
+ PrecacheScriptSound( "Passtime.Tv3" );
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::OnEnterGoal( CPasstimeBall *pBall, CFuncPasstimeGoal *pGoal )
+{
+ if ( pGoal->BDisableBallScore() || !IsGamestatePlayable() )
+ {
+ return;
+ }
+
+ // -1 iPoints is a special hacked value that means "kill zone"
+ if ( pGoal->Points() == -1 )
+ {
+ m_onBallRemoved.FireOutput( pGoal, pGoal );
+ SetContextThink( &CTFPasstimeLogic::RespawnBall, gpGlobals->curtime, "spawnball" );
+ return;
+ }
+
+ if ( (pBall->GetCollisionCount() > 0) || (pBall->GetTeamNumber() == TEAM_UNASSIGNED) )
+ {
+ return;
+ }
+
+ CTFPlayer *pOwner = pBall->GetThrower();
+ if ( pOwner && (pBall->GetTeamNumber() == pGoal->GetTeamNumber()) )
+ {
+ Score( pBall, pGoal );
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::OnEnterGoal( CTFPlayer *pPlayer, CFuncPasstimeGoal *pGoal )
+{
+ if ( IsGamestatePlayable()
+ && pGoal->BEnablePlayerScore()
+ && pPlayer->m_Shared.HasPasstimeBall()
+ && (pPlayer->GetTeamNumber() == pGoal->GetTeamNumber()) )
+ {
+ Score( pPlayer, pGoal );
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::OnExitGoal( CPasstimeBall *pBall, CFuncPasstimeGoal *pGoal )
+{
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::OnStayInGoal( CTFPlayer *pPlayer, CFuncPasstimeGoal *pGoal )
+{
+ OnEnterGoal( pPlayer, pGoal );
+}
+
+//-----------------------------------------------------------------------------
+bool CTFPasstimeLogic::OnBallCollision( CPasstimeBall *pBall, int index, gamevcollisionevent_t *pEvent )
+{
+ if ( !IsGamestatePlayable() )
+ {
+ return false;
+ }
+
+ // FIXME
+ //if ( pBall && pBall->BAnyControllerApplied() )
+ //{
+ // return false;
+ //}
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+bool CTFPasstimeLogic::BCanPlayerPickUpBall( CTFPlayer *pPlayer, HudNotification_t *pReason ) const
+{
+ if ( pReason ) *pReason = (HudNotification_t) 0;
+
+ const auto *pBall = m_hBall.Get();
+ if ( !pBall )
+ {
+ return false;
+ }
+
+ if ( !pPlayer || !IsGamestatePlayable() )
+ {
+ return false;
+ }
+
+ if ( pPlayer->m_Shared.InCond( TF_COND_INVULNERABLE )
+ || pPlayer->m_Shared.InCond( TF_COND_PHASE )
+ || pPlayer->m_Shared.InCond( TF_COND_INVULNERABLE_WEARINGOFF ) )
+ {
+ if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_INVULN;
+ return false;
+ }
+
+ if ( pPlayer->m_Shared.IsStealthed() )
+ {
+ if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_CLOAK;
+ return false;
+ }
+
+ // let disguised spies pick up enemy ball, which amounts to interception and fake passes
+ auto bEnemyBall = ( pBall->GetTeamNumber() != TEAM_UNASSIGNED )
+ && ( pBall->GetTeamNumber() != pPlayer->GetTeamNumber() );
+ if ( pPlayer->m_Shared.InCond( TF_COND_DISGUISED ) && !bEnemyBall )
+ {
+ if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_DISGUISE;
+ return false;
+ }
+
+ if ( pPlayer->m_Shared.IsCarryingObject() )
+ {
+ if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_CARRY;
+ return false;
+ }
+
+ if ( pPlayer->m_Shared.InCond( TF_COND_SELECTED_TO_TELEPORT ) )
+ {
+ if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_TELE;
+ return false;
+ }
+
+ if ( pPlayer->IsTaunting() )
+ {
+ if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_TAUNT;
+ return false;
+ }
+
+ if ( !pPlayer->IsAllowedToPickUpFlag()
+ || !pPlayer->IsAlive() // NOTE: it's possible to be !alive and !dead at the same time
+ || pPlayer->IsAwayFromKeyboard()
+ || pPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE )
+ || pPlayer->m_Shared.IsControlStunned() )
+ {
+ return false;
+ }
+
+ if ( pPlayer->m_bIsTeleportingUsingEurekaEffect )
+ {
+ if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_TELE;
+ return false;
+ }
+
+ CTFWeaponBase *pActiveWeapon = pPlayer->GetActiveTFWeapon();
+ if ( pActiveWeapon )
+ {
+ bool bCanHolster = pActiveWeapon->CanHolster()
+ && !( pActiveWeapon->IsReloading() && pActiveWeapon->ReloadsSingly() && pActiveWeapon->CanOverload() ); // semihack to fix beggars bazooka problems
+ if ( pActiveWeapon && ( pActiveWeapon->GetWeaponID() != TF_WEAPON_PASSTIME_GUN ) && !bCanHolster )
+ {
+ if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_HOLSTER;
+ return false;
+ }
+ }
+
+ if ( EntityIsInNoBallZone( pPlayer ) )
+ {
+ if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_OOB;
+ return false;
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+int CTFPasstimeLogic::UpdateTransmitState()
+{
+ return SetTransmitState( FL_EDICT_ALWAYS );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::RespawnBall()
+{
+ Assert( m_hBall );
+ if ( !m_hBall ) // paranoia
+ {
+ return;
+ }
+
+ ClearBallPower();
+
+ // TFGameRules only checks capture limit once per second, so this code can't rely on game state changing
+ int iScoreLimit = tf_passtime_scores_per_round.GetInt();
+ bool bGameOver = ( TFTeamMgr()->GetFlagCaptures( TF_TEAM_RED ) >= iScoreLimit )
+ || ( TFTeamMgr()->GetFlagCaptures( TF_TEAM_BLUE ) >= iScoreLimit );
+
+ gamerules_roundstate_t state = TFGameRules()->State_Get();
+ if ( bGameOver || (state == GR_STATE_GAME_OVER) || (state == GR_STATE_TEAM_WIN) || (state == GR_STATE_RESTART) )
+ {
+ m_hBall->SetStateOutOfPlay();
+ MoveBallToSpawner();
+ }
+ else if ( (state == GR_STATE_RND_RUNNING) || (state == GR_STATE_STALEMATE) )
+ {
+ // TODO just end the game if there's not enough time to respawn the ball
+ m_hBall->SetStateOutOfPlay();
+ MoveBallToSpawner();
+ CTeamRoundTimer *pTimer = TFGameRules()->GetActiveRoundTimer();
+ if ( !pTimer || ( pTimer->GetTimeRemaining() > m_iBallSpawnCountdownSec ) )
+ {
+ m_pRespawnCountdown->Start( m_iBallSpawnCountdownSec );
+ SpawnBallAtRandomSpawnerThink();
+ }
+ }
+ else // pre-round etc
+ {
+ SpawnBallAtRandomSpawner();
+ }
+
+ m_ballLastHeldTimes.RemoveAll();
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::SpawnBallAtRandomSpawnerThink()
+{
+ if ( TFGameRules()->State_Get() == GR_STATE_GAME_OVER )
+ {
+ m_hBall->SetStateOutOfPlay();
+ m_pRespawnCountdown->Disable();
+ }
+ else if ( m_pRespawnCountdown->Tick( 1 ) )
+ {
+ SpawnBallAtRandomSpawner();
+ }
+ else
+ {
+ SetContextThink( &CTFPasstimeLogic::SpawnBallAtRandomSpawnerThink, gpGlobals->curtime + 1, "spawnball" );
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::SpawnBallAtRandomSpawner()
+{
+ const auto &allSpawns = IPasstimeBallSpawnAutoList::AutoList();
+ int i = RandomInt( 0, allSpawns.Count() - 1 );
+ CPasstimeBallSpawn *pSpawner = static_cast< CPasstimeBallSpawn *>( allSpawns[i] );
+ SpawnBallAtSpawner( pSpawner );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::MoveBallToSpawner()
+{
+ const auto &allSpawns = IPasstimeBallSpawnAutoList::AutoList();
+ int i = RandomInt( 0, allSpawns.Count() - 1 );
+ CPasstimeBallSpawn *pSpawner = static_cast< CPasstimeBallSpawn *>( allSpawns[i] );
+ m_hBall->MoveTo( pSpawner->GetAbsOrigin(), Vector( 0, 0, 0 ) );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::SpawnBallAtSpawner( CPasstimeBallSpawn *pSpawner )
+{
+ if ( !m_hBall )
+ {
+ // NOTE: this is the first place where the ball is created - on first spawn
+ m_hBall = CPasstimeBall::Create( pSpawner->GetAbsOrigin(), QAngle(0,0,0) );
+ }
+
+ StopAskForBallEffects();
+ m_hBall->SetStateFree();
+ m_hBall->MoveToSpawner( pSpawner->GetAbsOrigin() );
+ m_hBall->ChangeTeam( pSpawner->GetTeamNumber() );
+ m_onBallFree.FireOutput( m_hBall, this );
+ pSpawner->m_onSpawnBall.FireOutput( pSpawner, pSpawner );
+
+ TFGameRules()->BroadcastSound( 255, "Passtime.BallSpawn" );
+ if ( TFGameRules()->IsHolidayActive( kHoliday_Halloween ) )
+ {
+ TFGameRules()->BroadcastSound( 255, "Passtime.Merasmus.Laugh" );
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::StopAskForBallEffects()
+{
+ for( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( pPlayer )
+ {
+ pPlayer->m_Shared.SetAskForBallTime( 0 );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::OnBallCarrierMeleeHit( CTFPlayer *pPlayer, CTFPlayer *pAttacker )
+{
+ // TODO refactor OnBallCarrierMeleeHit and OnBallCarrierDamaged for less copypasta
+
+ if ( !pPlayer || !pAttacker || (pPlayer == pAttacker) || !TFGameRules() )
+ {
+ // shouldn't happen, but who knows
+ return;
+ }
+
+ Assert( pPlayer->m_Shared.HasPasstimeBall() );
+ if ( !pPlayer->m_Shared.HasPasstimeBall() )
+ {
+ return;
+ }
+
+ if ( !pPlayer->InSameTeam( pAttacker) )
+ {
+ // currently handled by OnBallCarrierDamaged
+ return;
+ }
+
+ Assert( m_hBall );
+ if( !m_hBall )
+ {
+ return;
+ }
+
+ bool bTooLong = (m_hBall->GetCarryDuration() > tf_passtime_teammate_steal_time.GetFloat());
+ if ( pPlayer->m_bPasstimeBallSlippery || bTooLong )
+ {
+ // once a player has held the ball too long, mark them as a jerk
+ // so they can't hoard the ball ever again
+ StealBall( pPlayer, pAttacker );
+ pPlayer->m_bPasstimeBallSlippery = true;
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::OnBallCarrierDamaged( CTFPlayer *pPlayer, CTFPlayer *pAttacker,
+ const CTakeDamageInfo& info )
+{
+ // TODO refactor OnBallCarrierMeleeHit and OnBallCarrierDamaged for less copypasta
+
+ // NOTE: it's possible that neither player has the ball if the attacker
+ // killed the carrier, which would cause EjectBall to happen before
+ // this call. There's no good way around it.
+
+ if ( !pPlayer || !pAttacker || (pPlayer == pAttacker) || !TFGameRules() )
+ {
+ // happens from world damage
+ return;
+ }
+
+ //
+ // Only care about melee damage
+ //
+ // DMG_CLUB is demo charge
+ if ( !tf_passtime_steal_on_melee.GetBool() || !(info.GetDamageType() & (DMG_MELEE | DMG_CLUB)) )
+ {
+ return;
+ }
+
+ Assert( m_hBall );
+ if ( !m_hBall )
+ {
+ return;
+ }
+
+ if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BASEBALL )
+ {
+ auto launch = CPasstimeGun::CalcLaunch( pPlayer, false );
+ LaunchBall(pPlayer, launch.startPos, launch.startVel );
+ }
+ else
+ {
+ StealBall( pPlayer, pAttacker );
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::CrowdReactionSound( int iTeam )
+{
+ if ( m_flNextCrowdReactionTime <= gpGlobals->curtime )
+ {
+ TFGameRules()->BroadcastSound( iTeam, "Passtime.Crowd.React.Pos" );
+ TFGameRules()->BroadcastSound( GetEnemyTeam( iTeam ), "Passtime.Crowd.React.Neg" );
+ m_flNextCrowdReactionTime = gpGlobals->curtime + 10.0f;
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::StealBall( CTFPlayer *pFrom, CTFPlayer *pTo )
+{
+ if ( pFrom->m_Shared.HasPasstimeBall() )
+ {
+ EjectBall( pFrom, pTo );
+ }
+
+ HudNotification_t cantPickUpReason;
+ if ( BCanPlayerPickUpBall( pTo, &cantPickUpReason ) )
+ {
+ if ( !pFrom->m_bPasstimeBallSlippery )
+ {
+ CTF_GameStats.Event_PlayerAwardBonusPoints( pTo, pTo, 10 );
+ }
+
+ TFGameRules()->BroadcastSound( 255, "Passtime.BallStolen" );
+
+ ++CTF_GameStats.m_passtimeStats.summary.nTotalSteals;
+
+ m_hBall->SetStateCarried( pTo );
+ OnBallGet();
+ pTo->m_Shared.AddCond( TF_COND_PASSTIME_INTERCEPTION, tf_passtime_speedboost_on_get_ball_time.GetFloat() );
+
+ int pointsToAward = 5;
+ if ( CFuncPasstimeGoalieZone::BPlayerInAny( pTo ) )
+ {
+ ++CTF_GameStats.m_passtimeStats.summary.nTotalStealsNearGoal;
+ pointsToAward = 10; // Extra points for last second defend.
+ }
+
+ if ( !pFrom->m_bPasstimeBallSlippery )
+ {
+ CTF_GameStats.Event_PlayerAwardBonusPoints( pTo, 0, pointsToAward );
+ }
+
+ PasstimeGameEvents::BallStolen( pFrom->entindex(), pTo->entindex() ).Fire();
+ CrowdReactionSound( pTo->GetTeamNumber() );
+ }
+ else if ( cantPickUpReason )
+ {
+ CSingleUserReliableRecipientFilter filter( pTo );
+ TFGameRules()->SendHudNotification( filter, cantPickUpReason );
+ }
+}
+
+//-----------------------------------------------------------------------------
+float CTFPasstimeLogic::GetLastHeldTime( CTFPlayer* pPlayer )
+{
+ float lastHeldTime = 0.0f;
+ for ( int i = 0; i < m_ballLastHeldTimes.Count(); i++ )
+ {
+ if ( m_ballLastHeldTimes[i].first == pPlayer )
+ {
+ lastHeldTime = m_ballLastHeldTimes[i].second;
+ break;
+ }
+ }
+
+ return lastHeldTime;
+}
+
+//-----------------------------------------------------------------------------
+float CTFPasstimeLogic::GetLastPassTime( CTFPlayer* pPlayer )
+{
+ float lastPassTime = 0.0f;
+ for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ )
+ {
+ if ( m_ballLastPassTimes[i].first == pPlayer )
+ {
+ lastPassTime = m_ballLastPassTimes[i].second;
+ break;
+ }
+ }
+
+ return lastPassTime;
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::SetLastPassTime( CTFPlayer* pPlayer )
+{
+ std::pair<CTFPlayer*, float> toAdd( pPlayer, gpGlobals->realtime );
+ bool skipTheRest = false;
+ for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ )
+ {
+ if ( m_ballLastPassTimes[i].first == pPlayer )
+ {
+ m_ballLastPassTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector
+ skipTheRest = true;
+ break;
+ }
+ }
+
+ if ( !skipTheRest )
+ {
+ m_ballLastPassTimes.AddToTail( toAdd );
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::EjectBall( CTFPlayer *pPlayer, CTFPlayer *pAttacker )
+{
+ if ( !m_hBall )
+ {
+ // I'm not sure how this is possible, but if I'm recording with hltv in
+ // a listen server that has bots in it (which requires a hack in the bot
+ // concommand) and I restart the game while a bot is holding the ball...
+ // then m_hBall is invalid.
+ if ( pPlayer )
+ {
+ // This has to be true to get into this function for the case I just
+ // described above, and since the ball has been deleted somehow during
+ // the round restart, it probably isn't necessary to set this to 0
+ // because the player's going to be reset anyway. But I want to make
+ // sure it's correct.
+ pPlayer->m_Shared.SetHasPasstimeBall( 0 );
+ }
+ return;
+ }
+
+ m_hBall->SetStateFree();
+ m_hBall->ChangeTeam( TEAM_UNASSIGNED );
+
+ Vector vecEjectVel( 0, 0, 600 );
+ vecEjectVel += pPlayer->GetAbsVelocity() * 0.1f;
+ m_hBall->MoveTo( pPlayer->GetAbsOrigin() + Vector( 0, 0, 32 ), vecEjectVel );
+ if ( pPlayer != pAttacker )
+ {
+ pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_LOST_OBJECT );
+ pAttacker->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_TAUNTS );
+ }
+
+ m_onBallFree.FireOutput( m_hBall, this );
+ std::pair<CTFPlayer*, float> toAdd( pPlayer, gpGlobals->realtime );
+ for ( int i = 0; i < m_ballLastHeldTimes.Count(); i++ )
+ {
+ if ( m_ballLastHeldTimes[i].first == pPlayer )
+ {
+ m_ballLastHeldTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector
+ return;
+ }
+ }
+
+ m_ballLastHeldTimes.AddToTail( toAdd );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::LaunchBall( CTFPlayer *pPlayer, const Vector &vecPos, const Vector &vecVel )
+{
+ StopAskForBallEffects();
+ m_hBall->SetStateFree();
+ m_hBall->MoveTo( vecPos, vecVel );
+ m_onBallFree.FireOutput( m_hBall, this );
+ std::pair<CTFPlayer*, float> toAdd( pPlayer, gpGlobals->realtime );
+ for ( int i = 0; i < m_ballLastHeldTimes.Count(); i++ )
+ {
+ if ( m_ballLastHeldTimes[i].first == pPlayer )
+ {
+ m_ballLastHeldTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector
+ return;
+ }
+ }
+
+ m_ballLastHeldTimes.AddToTail( toAdd );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::Score( CTFPlayer *pPlayer, CFuncPasstimeGoal *pGoal )
+{
+ Assert( pPlayer && pGoal );
+ pGoal->OnScore( pPlayer->GetTeamNumber() );
+ Score( pPlayer, pGoal->GetTeamNumber(), pGoal->Points(), pGoal->BWinOnScore() );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::Score( CPasstimeBall *pBall, CFuncPasstimeGoal *pGoal )
+{
+ Assert( pBall && pGoal );
+ CTFPlayer* pPlayer = pBall->GetThrower();
+ Assert( pPlayer );
+ pGoal->OnScore( pPlayer->GetTeamNumber() );
+ Score( pPlayer, pGoal->GetTeamNumber(), pGoal->Points(), pGoal->BWinOnScore() );
+}
+
+//-----------------------------------------------------------------------------
+// static
+void CTFPasstimeLogic::AddCondToTeam( ETFCond eCond, int iTeam, float flTime )
+{
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( pTFPlayer && (pTFPlayer->GetTeamNumber() == iTeam) && pTFPlayer->IsAlive() )
+ {
+ pTFPlayer->m_Shared.AddCond( eCond, flTime );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::Score( CTFPlayer *pPlayer, int iTeam, int iPoints, bool bForceWin )
+{
+ StopAskForBallEffects();
+ m_pRespawnCountdown->Disable();
+
+ Assert( pPlayer );
+ if ( !pPlayer || ( iTeam == TEAM_UNASSIGNED ) )
+ {
+ return;
+ }
+
+ if ( bForceWin )
+ {
+ iPoints = MAX( 1, tf_passtime_scores_per_round.GetInt() - TFTeamMgr()->GetFlagCaptures( iTeam ) );
+ }
+
+ //
+ // Update stats
+ //
+ ++CTF_GameStats.m_passtimeStats.summary.nTotalScores;
+ ++CTF_GameStats.m_passtimeStats.classes[ pPlayer->GetPlayerClass()->GetClassIndex() ].nTotalScores;
+ CTF_GameStats.Event_PlayerCapturedPoint( pPlayer );
+
+ //
+ // Award player points
+ //
+ CTF_GameStats.Event_PlayerAwardBonusPoints( pPlayer, 0, 25 );
+
+ //
+ // Award player assist points
+ //
+ {
+ CTFPlayer *pAssister = nullptr;
+ float flAssisterTime = FLT_MAX;
+ for ( unsigned short i = 0; i < m_ballLastHeldTimes.Count(); i++ )
+ {
+ auto &tempPair = m_ballLastHeldTimes[i];
+ auto *pPossibleAssister = tempPair.first;
+ auto timeLastHeld = tempPair.second;
+ auto flSecAgo = gpGlobals->realtime - timeLastHeld;
+ if ( ( pPossibleAssister->GetTeamNumber() == pPlayer->GetTeamNumber() )
+ && ( pPossibleAssister != pPlayer )
+ && ( flSecAgo < 10.0f )
+ && ( flSecAgo < flAssisterTime ) )
+ {
+ pAssister = pPossibleAssister;
+ flAssisterTime = flSecAgo;
+ }
+ }
+
+ if ( pAssister )
+ {
+ CTF_GameStats.Event_PlayerAwardBonusPoints( pAssister, 0, 10 );
+ PasstimeGameEvents::Score( pPlayer->entindex(), pAssister->entindex(), iPoints ).Fire();
+ }
+ else
+ {
+ PasstimeGameEvents::Score( pPlayer->entindex(), iPoints ).Fire();
+ }
+ }
+
+ //
+ // Award team points
+ //
+ while ( iPoints-- > 0 )
+ {
+ TFTeamMgr()->IncrementFlagCaptures( iTeam );
+ }
+
+ //
+ // Award bonus conditions
+ //
+ AddCondToTeam( TF_COND_CRITBOOSTED_CTF_CAPTURE, pPlayer->GetTeamNumber(), tf_passtime_score_crit_sec.GetFloat() );
+
+ //
+ // Feedback
+ //
+ pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_FLAGCAPTURED );
+ TFGameRules()->BroadcastSound( iTeam, "Passtime.Crowd.Cheer" );
+ TFGameRules()->BroadcastSound( GetEnemyTeam( iTeam ), "Passtime.Crowd.Boo" );
+
+ if ( TFGameRules()->IsHolidayActive( kHoliday_Halloween ) )
+ {
+ const char* pszSound = ( iTeam == TF_TEAM_RED )
+ ? "sf14.Merasmus.Soccer.GoalRed"
+ : "sf14.Merasmus.Soccer.GoalBlue";
+ TFGameRules()->BroadcastSound( 255, pszSound );
+ }
+
+ //
+ // Game state management
+ //
+ ClearBallPower();
+ m_hBall->SetStateOutOfPlay();
+ MoveBallToSpawner(); // move it now instead of when it spawns to avoid lerping
+
+ //
+ // Finish round or respawn ball
+ //
+ CTeamRoundTimer *pRoundTimer = TFGameRules()->GetActiveRoundTimer();
+ if ( ( TFGameRules()->State_Get() == GR_STATE_STALEMATE ) || ( pRoundTimer && ( pRoundTimer->GetTimeRemaining() <= 0.0f ) ) )
+ {
+ EndRoundExpiredTimer();
+ }
+ else
+ {
+ SetContextThink( &CTFPasstimeLogic::RespawnBall, gpGlobals->curtime, "spawnball" );
+ }
+
+ //
+ // Fire outputs
+ //
+ m_onScoreAny.FireOutput( pPlayer, this );
+ if( iTeam == TF_TEAM_RED )
+ {
+ m_onScoreRed.FireOutput( pPlayer, this );
+ }
+ else if( iTeam == TF_TEAM_BLUE )
+ {
+ m_onScoreBlu.FireOutput( pPlayer, this );
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::OnPlayerTouchBall( CTFPlayer *pCatcher, CPasstimeBall *pBall )
+{
+ if ( pBall != m_hBall )
+ {
+ return;
+ }
+
+ const int iCatcherTeam = pCatcher->GetTeamNumber();
+ float flFeet = pBall->GetAirtimeDistance() / 16.0f;
+ auto iExperiment = (EPasstimeExperiment_Telepass) tf_passtime_experiment_telepass.GetInt();
+
+ //
+ // Check for pass and interception
+ //
+ CTFPlayer *pThrower = pBall->GetThrower();
+ if ( pThrower // ball must must have been thrown...
+ && (pBall->GetCollisionCount() == 0) // and not bounced...
+ && (pBall->GetTeamNumber() != TEAM_UNASSIGNED) // and not be neutral...
+ && (pCatcher != pBall->GetPrevCarrier())) // and not passed to yourself...
+ {
+ PasstimeGameEvents::PassCaught( pThrower->entindex(), pCatcher->entindex(), flFeet, pBall->GetAirtimeSec() ).Fire();
+
+ bool bAllowCheerSound = true;
+
+ int iDistanceBonus = ( int ) ( pBall->GetAirtimeSec() * tf_passtime_powerball_airtimebonus.GetFloat() );
+ iDistanceBonus = clamp( iDistanceBonus, 0, tf_passtime_powerball_maxairtimebonus.GetInt() );
+ int iPassPoints = tf_passtime_powerball_passpoints.GetInt();
+ AddBallPower( iPassPoints + iDistanceBonus );
+ bAllowCheerSound = m_iBallPower < tf_passtime_powerball_threshold.GetInt();
+
+ CPASFilter pasFilter( pCatcher->GetAbsOrigin() );
+ pCatcher->EmitSound( pasFilter, pCatcher->entindex(), "Passtime.BallCatch" );
+
+ // make sure this happens before BeginCarry/SEtOwner etc
+ if ( pThrower->GetTeamNumber() == iCatcherTeam )
+ {
+ if ( pBall->GetHomingTarget() )
+ {
+ // pass was caught by teammate
+ ++CTF_GameStats.m_passtimeStats.summary.nTotalPassesCompleted;
+ CTF_GameStats.m_passtimeStats.AddPassTravelDistSample( pBall->GetAirtimeDistance() );
+
+ // award bonus effects for pass
+ pCatcher->m_Shared.AddCond( TF_COND_SPEED_BOOST, tf_passtime_speedboost_on_get_ball_time.GetFloat() );
+
+ if ( CFuncPasstimeGoalieZone::BPlayerInAny( pCatcher ) )
+ {
+ ++CTF_GameStats.m_passtimeStats.summary.nTotalPassesCompletedNearGoal;
+ }
+
+ if ( iExperiment != EPasstimeExperiment_Telepass::None )
+ {
+ // origins need to be a copy
+ auto throwerOrigin = pThrower->GetAbsOrigin();
+ auto catcherOrigin = pCatcher->GetAbsOrigin();
+
+ CPVSFilter filterThrower( throwerOrigin );
+ switch( pThrower->GetTeamNumber() )
+ {
+ case TF_TEAM_RED:
+ TE_TFParticleEffect( filterThrower, 0.0, "teleported_red", throwerOrigin, vec3_angle );
+ TE_TFParticleEffect( filterThrower, 0.0, "player_sparkles_red", throwerOrigin, vec3_angle, pThrower, PATTACH_ABSORIGIN );
+ break;
+ case TF_TEAM_BLUE:
+ TE_TFParticleEffect( filterThrower, 0.0, "teleported_blue", throwerOrigin, vec3_angle );
+ TE_TFParticleEffect( filterThrower, 0.0, "player_sparkles_blue", throwerOrigin, vec3_angle, pThrower, PATTACH_ABSORIGIN );
+ break;
+ default:
+ break;
+ }
+
+ pThrower->EmitSound( "Building_Teleporter.Send" );
+ pCatcher->EmitSound( "Building_Teleporter.Receive" );
+
+ // then move the player
+ pThrower->Teleport( &catcherOrigin, nullptr, nullptr );
+ if ( iExperiment == EPasstimeExperiment_Telepass::SwapWithCatcher )
+ {
+ pCatcher->Teleport( &throwerOrigin, nullptr, nullptr );
+
+ CPVSFilter filterCatcher( catcherOrigin );
+ switch( pCatcher->GetTeamNumber() )
+ {
+ case TF_TEAM_RED:
+ TE_TFParticleEffect( filterCatcher, 0.0, "teleported_red", catcherOrigin, vec3_angle );
+ TE_TFParticleEffect( filterCatcher, 0.0, "player_sparkles_red", catcherOrigin, vec3_angle, pCatcher, PATTACH_ABSORIGIN );
+ break;
+ case TF_TEAM_BLUE:
+ TE_TFParticleEffect( filterCatcher, 0.0, "teleported_blue", catcherOrigin, vec3_angle );
+ TE_TFParticleEffect( filterCatcher, 0.0, "player_sparkles_blue", catcherOrigin, vec3_angle, pCatcher, PATTACH_ABSORIGIN );
+ break;
+ default:
+ break;
+ }
+ }
+
+ // then start the effects
+ pThrower->TeleportEffect();
+ }
+ }
+ else
+ {
+ // toss was caught by teammate
+ ++CTF_GameStats.m_passtimeStats.summary.nTotalTossesCompleted;
+ }
+
+ float lastPassTime = 0.0f;
+ for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ )
+ {
+ if ( m_ballLastPassTimes[i].first == pThrower)
+ {
+ lastPassTime = m_ballLastPassTimes[i].second;
+ break;
+ }
+ }
+
+ // successful pass
+ if ( flFeet > 30 )
+ {
+ // fanfare and points if the pass was long enough (and we haven't been spamming throw/catch for points)
+ if ( gpGlobals->realtime - lastPassTime > 6.0f ) // FIXME literal balance value
+ {
+ CTF_GameStats.Event_PlayerAwardBonusPoints( pThrower, pThrower, 15 ); // FIXME literal balance value
+ }
+
+ if ( bAllowCheerSound && ( pBall->GetAirtimeSec() > 2.0f ) ) // FIXME literal balance value
+ {
+ TFGameRules()->BroadcastSound( 255, "TFPlayer.StunImpactRange" );
+ }
+ }
+ else// flFeet <= 30
+ {
+ // (points conditional on we haven't had the ball in the last 6 seconds)
+ if ( gpGlobals->realtime - lastPassTime > 6.0f ) // FIXME literal balance value
+ {
+ CTF_GameStats.Event_PlayerAwardBonusPoints( pThrower, pThrower, 5 ); // FIXME literal balance value
+ }
+ }
+
+ std::pair<CTFPlayer*, float> toAdd( pThrower, gpGlobals->realtime );
+ bool skipTheRest = false;
+ for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ )
+ {
+ if ( m_ballLastPassTimes[i].first == pThrower)
+ {
+ m_ballLastPassTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector
+ skipTheRest = true;
+ break;
+ }
+ }
+
+ if ( !skipTheRest )
+ {
+ m_ballLastPassTimes.AddToTail( toAdd );
+ }
+ }
+ else
+ {
+ if ( pBall->GetHomingTarget() )
+ {
+ // pass was intercepted
+ ++CTF_GameStats.m_passtimeStats.summary.nTotalPassesIntercepted;
+ CTF_GameStats.m_passtimeStats.AddPassTravelDistSample( pBall->GetAirtimeDistance() );
+ }
+ else
+ {
+ // toss was intercepted
+ ++CTF_GameStats.m_passtimeStats.summary.nTotalTossesIntercepted;
+ }
+
+ // interception can happen at any range, extra points if intercepted within the goal area
+ int bonusPointsToAward = 15; // FIXME literal balance value
+ if ( CFuncPasstimeGoalieZone::BPlayerInAny( pCatcher ) )
+ {
+ bonusPointsToAward = 25; // FIXME literal balance value
+ if ( pBall->GetHomingTarget() )
+ {
+ ++CTF_GameStats.m_passtimeStats.summary.nTotalPassesInterceptedNearGoal;
+ }
+ else
+ {
+ ++CTF_GameStats.m_passtimeStats.summary.nTotalTossesInterceptedNearGoal;
+ }
+ }
+
+ // award bonus effects for interception
+ pCatcher->m_Shared.AddCond( TF_COND_PASSTIME_INTERCEPTION, tf_passtime_speedboost_on_get_ball_time.GetFloat() );
+ pCatcher->m_Shared.AddCond( TF_COND_SPEED_BOOST, tf_passtime_speedboost_on_get_ball_time.GetFloat() );
+
+ CTF_GameStats.Event_PlayerAwardBonusPoints( pCatcher, pCatcher, bonusPointsToAward );
+ TFGameRules()->BroadcastSound( 255, "Passtime.BallIntercepted" );
+ CrowdReactionSound( pCatcher->GetTeamNumber() );
+ }
+ }
+ else
+ {
+ ++CTF_GameStats.m_passtimeStats.summary.nTotalRecoveries;
+ CTFPlayer *pPrevCarrier = pBall->GetPrevCarrier();
+ if ( pCatcher != pPrevCarrier )
+ {
+ // Gain a point for picking up a neutral ball.
+ CTF_GameStats.Event_PlayerAwardBonusPoints( pCatcher, pThrower, 5 ); // FIXME literal balance value
+ }
+
+ PasstimeGameEvents::BallGet( pCatcher->entindex() ).Fire();
+ }
+
+ if ( ((iExperiment == EPasstimeExperiment_Telepass::TeleportToCatcherMaintainPossession)
+ || (iExperiment == EPasstimeExperiment_Telepass::SwapWithCatcher))
+ && BCanPlayerPickUpBall( pThrower, nullptr ) )
+ {
+ EjectBall( pCatcher, pThrower );
+ m_hBall->SetStateCarried( pThrower );
+ OnBallGet();
+ }
+ else
+ {
+ pBall->SetStateCarried( pCatcher );
+ OnBallGet();
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::OnBallGet()
+{
+ StopAskForBallEffects();
+ if ( CTFPlayer *pPlayer = m_hBall->GetCarrier() )
+ {
+ m_onBallGetAny.FireOutput( pPlayer, this );
+ if ( pPlayer->GetTeamNumber() == TF_TEAM_RED )
+ {
+ m_onBallGetRed.FireOutput( pPlayer, this );
+ }
+ else if ( pPlayer->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ m_onBallGetBlu.FireOutput( pPlayer, this );
+ }
+ CPasstimeBallController::BallPickedUp( m_hBall, pPlayer );
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::InputSpawnBall( inputdata_t &input )
+{
+ RespawnBall();
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::InputTimeUp( inputdata_t &input )
+{
+ int iRedScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_RED );
+ int iBlueScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_BLUE );
+ int iPointDifference = abs( iRedScore - iBlueScore );
+
+ // going through the list of goals to calculate the max possible point gain
+ // is possible but tricky since goals can be enabled/disabled and there's no
+ // way to know which goals are actually possible to score in, so this is
+ // simply hard-coded to work correctly for the official maps where there's
+ // a 3-point unlockable goal.
+ int iMaxPossibleScoreGain = 3;
+
+ if ( ( iPointDifference <= iMaxPossibleScoreGain ) && !ShouldEndOvertime() )
+ {
+ m_pRespawnCountdown->Disable();
+ TFGameRules()->BroadcastSound( 255, "Game.Overtime" );
+ ThinkExpiredTimer();
+ }
+ else
+ {
+ EndRoundExpiredTimer();
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::ThinkExpiredTimer()
+{
+ if ( TFGameRules() && (TFGameRules()->State_Get() != GR_STATE_RND_RUNNING) )
+ {
+ if ( m_pRespawnCountdown )
+ {
+ // just in case
+ m_pRespawnCountdown->Disable();
+ }
+ return;
+ }
+
+ if ( ShouldEndOvertime() || m_pRespawnCountdown->Tick( gpGlobals->frametime ) )
+ {
+ EndRoundExpiredTimer();
+ return;
+ }
+
+ // Check again every frame until either something else ends the round
+ // or the conditions are met that allow an expired timer to end the round.
+ SetContextThink( &CTFPasstimeLogic::ThinkExpiredTimer, gpGlobals->curtime, "ThinkExpiredTimer" );
+
+ Assert( m_hBall ); // verified in ShouldEndOvertime
+ Assert( m_pRespawnCountdown ); // always valid after Spawn
+ bool bBallUnassigned = m_hBall->GetTeamNumber() == TEAM_UNASSIGNED;
+ bool bCountdownRunning = !m_pRespawnCountdown->IsDisabled();
+ if ( bBallUnassigned && !bCountdownRunning )
+ {
+ // start the countdown when the ball turns neutral
+ m_pRespawnCountdown->Start( tf_passtime_overtime_idle_sec.GetFloat() );
+ }
+ else if ( !bBallUnassigned && bCountdownRunning )
+ {
+ // stop the countdown when the ball is picked up
+ m_pRespawnCountdown->Disable();
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+bool CTFPasstimeLogic::ShouldEndOvertime() const
+{
+ if ( !m_hBall || !TFGameRules() )
+ {
+ return true;
+ }
+
+ // if nobody has the ball, only the respawn countdown can end overtime
+ CTFPlayer *pBallCarrier = m_hBall->GetCarrier();
+ if ( m_hBall->GetTeamNumber() == TEAM_UNASSIGNED || !pBallCarrier )
+ {
+ return false;
+ }
+
+ // if the teams are tied, someone has to score
+ int iRedScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_RED );
+ int iBluScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_BLUE );
+ if ( iRedScore == iBluScore )
+ {
+ return false;
+ }
+
+ // if the winning team has posession, they win
+ int iWinningTeam = ( iRedScore > iBluScore )
+ ? TF_TEAM_RED
+ : TF_TEAM_BLUE;
+ return pBallCarrier->GetTeamNumber() == iWinningTeam;
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::EndRoundExpiredTimer()
+{
+ StopAskForBallEffects();
+ m_pRespawnCountdown->Disable();
+ SetContextThink( &CTFPasstimeLogic::ThinkExpiredTimer, TICK_NEVER_THINK, "ThinkExpiredTimer" );
+
+ // copied from TeamplayRoundBasedGameRules::State_Think_RND_RUNNING
+ int iDrawScoreCheck = -1;
+ int iWinningTeam = 0;
+ bool bTeamsAreDrawn = true;
+ for ( int i = FIRST_GAME_TEAM; (i < GetNumberOfTeams()) && bTeamsAreDrawn; i++ )
+ {
+ int iTeamScore = TFTeamMgr()->GetFlagCaptures( i );
+
+ if ( iTeamScore > iDrawScoreCheck )
+ {
+ iWinningTeam = i;
+ }
+
+ if ( iTeamScore != iDrawScoreCheck )
+ {
+ if ( iDrawScoreCheck == -1 )
+ {
+ iDrawScoreCheck = iTeamScore;
+ }
+ else
+ {
+ bTeamsAreDrawn = false;
+ }
+ }
+ }
+
+ if ( bTeamsAreDrawn )
+ {
+ TFGameRules()->SetStalemate( STALEMATE_SERVER_TIMELIMIT, true );
+ }
+ else
+ {
+ TFGameRules()->SetWinningTeam( iWinningTeam, WINREASON_TIMELIMIT, true, false, false );
+ }
+}
+
+//-----------------------------------------------------------------------------
+struct SetSectionParams
+{
+ int num;
+ CPathTrack *pSectionStart;
+ CPathTrack *pSectionEnd;
+ SetSectionParams() : num(-1), pSectionStart(0), pSectionEnd(0) {}
+};
+
+//-----------------------------------------------------------------------------
+bool CTFPasstimeLogic::ParseSetSection( const char *pStr, SetSectionParams &s ) const
+{
+ char pszStartName[64];
+ char pszEndName[64];
+ const int iScanCount = sscanf( pStr, "%i %s %s", &s.num, pszStartName, pszEndName ); // WHAT YEAR IS IT
+ if ( iScanCount != 3 )
+ {
+ return false;
+ }
+ s.pSectionStart = dynamic_cast<CPathTrack*>( gEntList.FindEntityByName( 0, pszStartName ) );
+ s.pSectionEnd = dynamic_cast<CPathTrack*>( gEntList.FindEntityByName( 0, pszEndName ) );
+
+ if ( s.num < 0 )
+ Warning( "SetSection number (%i) must be > 0\n", s.num );
+ if ( s.num >= m_iNumSections )
+ Warning( "SetSection number (%i) must be < section count (%i)\n", s.num, m_iNumSections.Get() );
+ if ( !s.pSectionStart )
+ Warning( "Failed to find section start path_track named %s\n", pszStartName );
+ if ( !s.pSectionEnd)
+ Warning( "Failed to find section end path_track named %s\n", pszEndName );
+
+ return (s.num >= 0)
+ && (s.num < m_iNumSections)
+ && s.pSectionStart
+ && s.pSectionEnd;
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::InputSetSection( inputdata_t &input )
+{
+ SetSectionParams params;
+ if ( !ParseSetSection( input.value.String(), params ) )
+ {
+ Warning( "Error in SetSection input: %s\n", input.value.String() );
+ return;
+ }
+
+ for ( int i = 0; i < m_trackPoints.Count(); ++i )
+ {
+ m_trackPoints.GetForModify(i).Zero();
+ }
+
+ int iTrackPoint = 0;
+ for ( CPathTrack *pTrack = params.pSectionStart; pTrack; pTrack = pTrack->GetNext(), ++iTrackPoint )
+ {
+ if ( iTrackPoint == m_trackPoints.Count() )
+ {
+ Warning( "Too many track_path in section (%i max, easily changed but must be fixed).", m_trackPoints.Count() );
+ return;
+ }
+
+ m_trackPoints.Set( iTrackPoint, pTrack->GetAbsOrigin() );
+ if ( pTrack->GetAbsOrigin() == Vector( 0, 0, 0 ) )
+ {
+ // Because I'm using 0,0,0 to represent "no point" in a fixed 16-element array
+ Warning( "Can't have track_path at 0,0,0" );
+ }
+
+ if ( pTrack == params.pSectionEnd )
+ {
+ break;
+ }
+ }
+
+ m_iCurrentSection = params.num;
+
+}
+
+
+//
+// Secret Room
+//
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::SecretRoom_Spawn()
+{
+ SECRETROOM_LOG( "@@@@ SECRET ROOM: Spawn\n" );
+ m_SecretRoom_pTv = gEntList.FindEntityByName( nullptr, "tv" );
+
+ string_t self = GetEntityName();
+
+ // plug_breakable.OnDamaged -> this.InputPlugDamaged
+ HookOutput( "plug_breakable", self, "OnDamaged", "staticc", nullptr, 1 );
+
+ // player triggers
+ // the names are generated gibberish words
+ // (Blu) (1)Scout: "comillow"
+ HookOutput( "comillow", self, "OnStartTouch", "statica" );
+ HookOutput( "comillow", self, "OnEndTouch", "staticb" );
+
+ // (Red) (2)Soldier: "unissubs"
+ HookOutput( "unissubs", self, "OnStartTouch", "statica" );
+ HookOutput( "unissubs", self, "OnEndTouch", "staticb" );
+
+ // (Red) (3)Pyro: "amment"
+ HookOutput( "amment", self, "OnStartTouch", "statica" );
+ HookOutput( "amment", self, "OnEndTouch", "staticb" );
+
+ // (Blu) (4)Demo: "memagold"
+ HookOutput( "memagold", self, "OnStartTouch", "statica" );
+ HookOutput( "memagold", self, "OnEndTouch", "staticb" );
+
+ // (Red) (5)Heavy: "subcla"
+ HookOutput( "subcla", self, "OnStartTouch", "statica" );
+ HookOutput( "subcla", self, "OnEndTouch", "staticb" );
+
+ // (Blu) (6)Engineer: "enempose"
+ HookOutput( "enempose", self, "OnStartTouch", "statica" );
+ HookOutput( "enempose", self, "OnEndTouch", "staticb" );
+
+ // (Red) (7)Medic: "irlenous"
+ HookOutput( "irlenous", self, "OnStartTouch", "statica" );
+ HookOutput( "irlenous", self, "OnEndTouch", "staticb" );
+
+ // (Red) (8)Sniper: "donked"
+ HookOutput( "donked", self, "OnStartTouch", "statica" );
+ HookOutput( "donked", self, "OnEndTouch", "staticb" );
+
+ // (Blu) (9)Spy: "finear"
+ HookOutput( "finear", self, "OnStartTouch", "statica" );
+ HookOutput( "finear", self, "OnEndTouch", "staticb" );
+
+ // the room trigger for keeping track of who gets the achievement
+ HookOutput( "room_trigger", self, "OnStartTouch", "RoomTriggerOnTouch" );
+ g_EventQueue.AddEvent( "room_trigger", "Enable", variant_t(), 0.0f, this, this );
+}
+
+//-----------------------------------------------------------------------------
+int CTFPasstimeLogic::SecretRoom_CountSlottedPlayers() const
+{
+ int iNumSlotsFilled = 0;
+ for ( CTFPlayer *pPlayer : m_SecretRoom_slottedPlayers )
+ {
+ if ( pPlayer ) ++iNumSlotsFilled;
+ }
+ return iNumSlotsFilled;
+}
+
+//-----------------------------------------------------------------------------
+// this doesn't need a template, but something like this in variant_t.h would
+// be nice. Or maybe just some explicit overloaded constructors.
+template <typename T> variant_t make_variant( T value );
+template <> variant_t make_variant( int value )
+{
+ variant_t v;
+ v.SetInt( value );
+ return v;
+}
+
+//-----------------------------------------------------------------------------
+static void SecretRoom_PlayTvSound( CSoundPatch **ppPatch, int iEntIndex, const char *pSoundName, float flVolume )
+{
+ Assert( ppPatch );
+ Assert( iEntIndex > 0 );
+ Assert( pSoundName && *pSoundName );
+ Assert( flVolume > 0 );
+
+ CSoundEnvelopeController &snd = CSoundEnvelopeController::GetController();
+ if ( *ppPatch )
+ {
+ SECRETROOM_LOG( " @@ SECRET ROOM: Destroy sound patch\n" );
+ snd.SoundDestroy( *ppPatch );
+ *ppPatch = nullptr;
+ }
+
+ SECRETROOM_LOG( " @@ SECRET ROOM: Create sound patch for %s volume %f\n", pSoundName, flVolume );
+ CReliableBroadcastRecipientFilter filter;
+ *ppPatch = snd.SoundCreate( filter, iEntIndex, pSoundName );
+ snd.Play( *ppPatch, flVolume, PITCH_NORM );
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::SecretRoom_UpdateTv( int iNumSlotsFilled )
+{
+ if ( iNumSlotsFilled == 9 )
+ {
+ SECRETROOM_LOG( " @@ SECRET ROOM: Update TV all slots filled\n" );
+ g_EventQueue.AddEvent( "screen", "Skin", make_variant( 3 ), 0.0f, this, this );
+ SecretRoom_PlayTvSound( &m_SecretRoom_pTvSound,
+ m_SecretRoom_pTv->entindex(), "Passtime.Tv3", 1.0f );
+ }
+ else
+ {
+ // sound
+ float volume = (float)( iNumSlotsFilled + 1 ) / 10.0f;
+ const char *pSoundName = ( iNumSlotsFilled >= 4 )
+ ? "Passtime.Tv2"
+ : "Passtime.Tv1";
+
+ SECRETROOM_LOG( " @@ SECRET ROOM: Update TV %i slots filled\n", iNumSlotsFilled );
+
+ SecretRoom_PlayTvSound( &m_SecretRoom_pTvSound,
+ m_SecretRoom_pTv->entindex(), pSoundName, volume );
+
+ // skin
+ int iSkin = ( iNumSlotsFilled >= 4 ) ? 2 : 1;
+ g_EventQueue.AddEvent( "screen", "Skin", make_variant( iSkin ), 0.0f, this, this );
+ }
+}
+
+//-----------------------------------------------------------------------------
+struct SecretRoom_TriggerInfo
+{
+ int iIndex;
+ const char *pTriggerName;
+ int iClass;
+ int iTeam;
+} static const s_SecretRoom_TriggerInfo[9] =
+{
+ { 0, "comillow", TF_CLASS_SCOUT, TF_TEAM_BLUE },
+ { 1, "unissubs", TF_CLASS_SOLDIER, TF_TEAM_RED },
+ { 2, "amment", TF_CLASS_PYRO, TF_TEAM_RED },
+ { 3, "memagold", TF_CLASS_DEMOMAN, TF_TEAM_BLUE },
+ { 4, "subcla", TF_CLASS_HEAVYWEAPONS, TF_TEAM_RED },
+ { 5, "enempose", TF_CLASS_ENGINEER, TF_TEAM_BLUE },
+ { 6, "irlenous", TF_CLASS_MEDIC, TF_TEAM_RED },
+ { 7, "donked", TF_CLASS_SNIPER, TF_TEAM_RED },
+ { 8, "finear", TF_CLASS_SPY, TF_TEAM_BLUE },
+};
+
+//-----------------------------------------------------------------------------
+static const SecretRoom_TriggerInfo &SecretRoom_GetSlotInfoForTrigger(
+ const char *pTriggerName )
+{
+ for ( const auto &info : s_SecretRoom_TriggerInfo )
+ {
+ if ( !V_strcmp( info.pTriggerName, pTriggerName ) )
+ {
+ return info;
+ }
+ }
+
+ Error( "Invalid trigger" );
+
+ // in case some platforms don't have noreturn attribute on Error
+ static SecretRoom_TriggerInfo unused;
+ return unused;
+}
+
+//-----------------------------------------------------------------------------
+// SecretRoom_InputStartTouchPlayerSlot
+void CTFPasstimeLogic::statica( inputdata_t &input )
+{
+ SECRETROOM_LOG( "@@@@ SECRET ROOM: Start touch player slot\n" );
+
+ if ( m_SecretRoom_state != SecretRoomState::Open )
+ {
+ SECRETROOM_LOG( " @ SECRET ROOM: Ignore - state is not open\n" );
+
+ // shouldn't happen because triggers should be disabled
+ return;
+ }
+
+ if ( !input.pCaller || !input.pActivator )
+ {
+ SECRETROOM_LOG( " @ SECRET ROOM: Ignore - no caller or activator\n" );
+
+ return;
+ }
+
+ CTFPlayer *pActivator = ToTFPlayer( input.pActivator );
+ SECRETROOM_LOG( " @ SECRET ROOM: Toucher is %s\n", pActivator->GetPlayerName() );
+
+ if ( !pActivator || pActivator->IsDead() || !pActivator->IsAlive() )
+ {
+ SECRETROOM_LOG( " @ SECRET ROOM: Ignore - bad player\n" );
+
+ // not a player or not normal
+ return;
+ }
+
+ const char *pTriggerName = input.pCaller->GetEntityName().ToCStr();
+ const auto& info = SecretRoom_GetSlotInfoForTrigger( pTriggerName );
+
+ SECRETROOM_LOG( " @ SECRET ROOM: Trigger is %s, slot is %i\n", pTriggerName, info.iIndex );
+
+
+ if ( m_SecretRoom_slottedPlayers[info.iIndex] )
+ {
+ SECRETROOM_LOG( " @ SECRET ROOM: Ignore - slot already filled by %s\n", m_SecretRoom_slottedPlayers[info.iIndex]->GetPlayerName() );
+
+ // already someone filling the slot
+ return;
+ }
+
+ int iActivatorTeam = pActivator->GetTeamNumber();
+ int iActivatorClass = pActivator->GetPlayerClass()->GetClassIndex();
+
+ if ( !pActivator
+ || ( info.iTeam != iActivatorTeam )
+ || ( info.iClass != iActivatorClass ) )
+ {
+ SECRETROOM_LOG( " @ SECRET ROOM: Ignore - wrong class %i (%i) or team %i (%i) \n",
+ iActivatorTeam, info.iTeam, info.iClass, iActivatorClass );
+
+ // doesn't match
+ return;
+ }
+
+ SECRETROOM_LOG( " @ SECRET ROOM: Set slot %i to %s\n", info.iIndex, pActivator->GetPlayerName() );
+
+ // set slot
+ m_SecretRoom_slottedPlayers[info.iIndex] = pActivator;
+
+ // either solve puzzle or update effects
+ int iNumSlotsFilled = SecretRoom_CountSlottedPlayers();
+ SECRETROOM_LOG( " @ SECRET ROOM: %i slots filled\n", iNumSlotsFilled );
+
+ if ( iNumSlotsFilled == 9 )
+ {
+ SecretRoom_Solve();
+ }
+ else
+ {
+ SecretRoom_UpdateTv( iNumSlotsFilled );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// SecretRoom_InputEndTouchPlayerSlot
+void CTFPasstimeLogic::staticb( inputdata_t &input )
+{
+ SECRETROOM_LOG( "@@@@ SECRET ROOM: End touch player slot\n" );
+
+ if ( m_SecretRoom_state != SecretRoomState::Open )
+ {
+ SECRETROOM_LOG( " @ SECRET ROOM: Ignore - state is not open\n" );
+
+ // shouldn't happen because triggers should be disabled
+ return;
+ }
+
+ const char *pTriggerName = input.pCaller->GetEntityName().ToCStr();
+ const auto& info = SecretRoom_GetSlotInfoForTrigger( pTriggerName );
+
+ SECRETROOM_LOG( " @ SECRET ROOM: Trigger is %s, slot is %i\n", pTriggerName, info.iIndex );
+
+
+ // input.pActivator can be null if a player disconnects while inside
+ // the trigger. but there's no way to tell if it's the player occupying
+ // the slot, so clear the slot just in case
+ if ( input.pActivator )
+ {
+ CTFPlayer *pActivator = ToTFPlayer( input.pActivator );
+ SECRETROOM_LOG( " @ SECRET ROOM: Toucher is %s\n", pActivator->GetPlayerName() );
+
+ if ( !pActivator || pActivator->IsDead() || !pActivator->IsAlive() )
+ {
+ SECRETROOM_LOG( " @ SECRET ROOM: Ignore - bad player\n" );
+
+ // not a player or not normal
+ return;
+ }
+
+ if ( m_SecretRoom_slottedPlayers[info.iIndex] != input.pActivator )
+ {
+ if ( m_SecretRoom_slottedPlayers[info.iIndex] )
+ SECRETROOM_LOG( " @ SECRET ROOM: Ignore - slot is held by %s\n", m_SecretRoom_slottedPlayers[info.iIndex]->GetPlayerName() );
+ else
+ SECRETROOM_LOG( " @ SECRET ROOM: Ignore - slot is empty\n" );
+
+ // slot is empty already or some other player exiting the trigger
+
+ // if slot is empty: due to this code not using proper filters,
+ // this can be caused by players suiciding after changing teams
+ // while standing inside the trigger, because the suicide happens
+ // after the team change. this case is the entire reason for
+ // m_SecretRoom_slottedPlayers.
+ return;
+ }
+ }
+
+ // clear the slot
+ // note: in the case where two matching players are in the trigger
+ // and the one that entered first exits, the remaining player won't count
+ // and will have to re-enter the trigger
+ SECRETROOM_LOG( " @ SECRET ROOM: Clear slot %i\n", info.iIndex );
+
+ m_SecretRoom_slottedPlayers[info.iIndex] = nullptr;
+
+ // update effects
+ SecretRoom_UpdateTv( SecretRoom_CountSlottedPlayers() );
+}
+
+//-----------------------------------------------------------------------------
+// SecretRoom_InputPlugDamaged
+void CTFPasstimeLogic::staticc( inputdata_t &input )
+{
+ SECRETROOM_LOG( "@@@@ SECRET ROOM: Plug destroyed\n" );
+
+ NOTE_UNUSED( input );
+ m_SecretRoom_state = SecretRoomState::Open;
+
+ // set fx for puzzle open
+ SecretRoom_UpdateTv( 0 );
+
+ // enable triggers
+ for ( const auto& info : s_SecretRoom_TriggerInfo )
+ {
+ g_EventQueue.AddEvent( info.pTriggerName, "Enable",
+ variant_t(), 0.0f, this, this );
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::SecretRoom_Solve()
+{
+ if ( m_SecretRoom_state != SecretRoomState::Open )
+ {
+ // paranoia
+ Assert( m_SecretRoom_state == SecretRoomState::Open );
+ return;
+ }
+
+ SECRETROOM_LOG( "@@@@ SECRET ROOM: Solved\n" );
+
+ m_SecretRoom_state = SecretRoomState::Solved;
+
+ // set fx for puzzle solved
+ g_EventQueue.AddEvent( "light", "TurnOn", variant_t(), 0.0f, this, this );
+ g_EventQueue.AddEvent( "spotlight", "LightOn", variant_t(), 0.0f, this, this );
+ g_EventQueue.AddEvent( "tv_particles", "Start", variant_t(), 0.0f, this, this );
+ g_EventQueue.AddEvent( "screen_image", "Enable", variant_t(), 0.0f, this, this );
+ SecretRoom_UpdateTv( 9 );
+
+ // disable triggers
+ for ( const auto& info : s_SecretRoom_TriggerInfo )
+ {
+ g_EventQueue.AddEvent( info.pTriggerName, "Disable",
+ variant_t(), 0.0f, this, this );
+ }
+
+ // achieves
+ for ( auto id : m_SecretRoom_playersThatTouchedRoom )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( GetPlayerBySteamID( id ) );
+ if ( pPlayer )
+ {
+ pPlayer->AwardAchievement( ACHIEVEMENT_TF_PASS_TIME_HAT );
+ }
+ }
+ m_SecretRoom_playersThatTouchedRoom.RemoveAll(); // paranoia
+}
+
+//-----------------------------------------------------------------------------
+void CTFPasstimeLogic::InputRoomTriggerOnTouch( inputdata_t &input )
+{
+ CTFPlayer *pPlayer = ToTFPlayer( input.pActivator );
+ if ( !pPlayer || pPlayer->IsBot() )
+ {
+ return;
+ }
+
+ CSteamID id;
+ pPlayer->GetSteamID( &id );
+ if ( id.IsValid() && ( m_SecretRoom_playersThatTouchedRoom.Find( id ) == -1 ) )
+ {
+ SECRETROOM_LOG( "@@@@ SECRET ROOM: Tracking %s for achievement\n", pPlayer->GetPlayerName() );
+ m_SecretRoom_playersThatTouchedRoom.AddToTail( id );
+ }
+}