//========= 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( goals[0] ); CFuncPasstimeGoal *pBlu = static_cast( 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 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 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 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 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( gEntList.FindEntityByName( 0, pszStartName ) ); s.pSectionEnd = dynamic_cast( 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 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 ); } }