diff options
Diffstat (limited to 'game/server/tf/tf_passtime_logic.cpp')
| -rw-r--r-- | game/server/tf/tf_passtime_logic.cpp | 2312 |
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 ); + } +} |