//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //===========================================================================// #include "cbase.h" #include "team_train_watcher.h" #include "team_control_point.h" #include "trains.h" #include "team_objectiveresource.h" #include "teamplayroundbased_gamerules.h" #include "team_control_point.h" #include "team_control_point_master.h" #include "engine/IEngineSound.h" #include "soundenvelope.h" #include "mp_shareddefs.h" #include "props.h" #include "physconstraint.h" #ifdef TF_DLL #include "tf_shareddefs.h" #endif // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" /* #define TWM_FIRSTSTAGEOUTCOME01 "Announcer.PLR_FirstStageOutcome01" #define TWM_FIRSTSTAGEOUTCOME02 "Announcer.PLR_FirstStageOutcome02" #define TWM_RACEGENERAL01 "Announcer.PLR_RaceGeneral01" #define TWM_RACEGENERAL02 "Announcer.PLR_RaceGeneral02" #define TWM_RACEGENERAL03 "Announcer.PLR_RaceGeneral03" #define TWM_RACEGENERAL04 "Announcer.PLR_RaceGeneral04" #define TWM_RACEGENERAL05 "Announcer.PLR_RaceGeneral05" #define TWM_RACEGENERAL08 "Announcer.PLR_RaceGeneral08" #define TWM_RACEGENERAL06 "Announcer.PLR_RaceGeneral06" #define TWM_RACEGENERAL07 "Announcer.PLR_RaceGeneral07" #define TWM_RACEGENERAL09 "Announcer.PLR_RaceGeneral09" #define TWM_RACEGENERAL12 "Announcer.PLR_RaceGeneral12" #define TWM_RACEGENERAL13 "Announcer.PLR_RaceGeneral13" #define TWM_RACEGENERAL14 "Announcer.PLR_RaceGeneral14" #define TWM_RACEGENERAL15 "Announcer.PLR_RaceGeneral15" #define TWM_RACEGENERAL10 "Announcer.PLR_RaceGeneral10" #define TWM_RACEGENERAL11 "Announcer.PLR_RaceGeneral11" #define TWM_SECONDSTAGEOUTCOME01 "Announcer.PLR_SecondStageOutcome01" #define TWM_SECONDSTAGEOUTCOME04 "Announcer.PLR_SecondStageOutcome04" #define TWM_SECONDSTAGEOUTCOME02 "Announcer.PLR_SecondStageOutcome02" #define TWM_SECONDSTAGEOUTCOME03 "Announcer.PLR_SecondStageOutcome03" #define TWM_FINALSTAGEOUTCOME01 "Announcer.PLR_FinalStageOutcome01" #define TWM_FINALSTAGEOUTCOME02 "Announcer.PLR_FinalStageOutcome02" #define TWM_FINALSTAGESTART01 "Announcer.PLR_FinalStageStart01" #define TWM_FINALSTAGESTART04 "Announcer.PLR_FinalStageStart04" #define TWM_FINALSTAGESTART08 "Announcer.PLR_FinalStageStart08" #define TWM_FINALSTAGESTART09 "Announcer.PLR_FinalStageStart09" #define TWM_FINALSTAGESTART07 "Announcer.PLR_FinalStageStart07" #define TWM_FINALSTAGESTART02 "Announcer.PLR_FinalStageStart02" #define TWM_FINALSTAGESTART03 "Announcer.PLR_FinalStageStart03" #define TWM_FINALSTAGESTART05 "Announcer.PLR_FinalStageStart05" #define TWM_FINALSTAGESTART06 "Announcer.PLR_FinalStageStart06" EHANDLE g_hTeamTrainWatcherMaster = NULL; */ #define MAX_ALARM_TIME_NO_RECEDE 18 // max amount of time to play the alarm if the train isn't going to recede BEGIN_DATADESC( CTeamTrainWatcher ) // Inputs. DEFINE_INPUTFUNC( FIELD_VOID, "RoundActivate", InputRoundActivate ), DEFINE_INPUTFUNC( FIELD_INTEGER, "SetNumTrainCappers", InputSetNumTrainCappers ), DEFINE_INPUTFUNC( FIELD_VOID, "OnStartOvertime", InputOnStartOvertime ), DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeedForwardModifier", InputSetSpeedForwardModifier ), DEFINE_INPUTFUNC( FIELD_INTEGER, "SetTrainRecedeTime", InputSetTrainRecedeTime ), DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetTrainCanRecede", InputSetTrainCanRecede ), DEFINE_INPUTFUNC( FIELD_INTEGER, "SetTrainRecedeTimeAndUpdate", InputSetTrainRecedeTimeAndUpdate ), // Outputs DEFINE_OUTPUT( m_OnTrainStartRecede, "OnTrainStartRecede" ), // key DEFINE_KEYFIELD( m_iszTrain, FIELD_STRING, "train" ), DEFINE_KEYFIELD( m_iszStartNode, FIELD_STRING, "start_node" ), DEFINE_KEYFIELD( m_iszGoalNode, FIELD_STRING, "goal_node" ), DEFINE_KEYFIELD( m_iszLinkedPathTracks[0], FIELD_STRING, "linked_pathtrack_1" ), DEFINE_KEYFIELD( m_iszLinkedCPs[0], FIELD_STRING, "linked_cp_1" ), DEFINE_KEYFIELD( m_iszLinkedPathTracks[1], FIELD_STRING, "linked_pathtrack_2" ), DEFINE_KEYFIELD( m_iszLinkedCPs[1], FIELD_STRING, "linked_cp_2" ), DEFINE_KEYFIELD( m_iszLinkedPathTracks[2], FIELD_STRING, "linked_pathtrack_3" ), DEFINE_KEYFIELD( m_iszLinkedCPs[2], FIELD_STRING, "linked_cp_3" ), DEFINE_KEYFIELD( m_iszLinkedPathTracks[3], FIELD_STRING, "linked_pathtrack_4" ), DEFINE_KEYFIELD( m_iszLinkedCPs[3], FIELD_STRING, "linked_cp_4" ), DEFINE_KEYFIELD( m_iszLinkedPathTracks[4], FIELD_STRING, "linked_pathtrack_5" ), DEFINE_KEYFIELD( m_iszLinkedCPs[4], FIELD_STRING, "linked_cp_5" ), DEFINE_KEYFIELD( m_iszLinkedPathTracks[5], FIELD_STRING, "linked_pathtrack_6" ), DEFINE_KEYFIELD( m_iszLinkedCPs[5], FIELD_STRING, "linked_cp_6" ), DEFINE_KEYFIELD( m_iszLinkedPathTracks[6], FIELD_STRING, "linked_pathtrack_7" ), DEFINE_KEYFIELD( m_iszLinkedCPs[6], FIELD_STRING, "linked_cp_7" ), DEFINE_KEYFIELD( m_iszLinkedPathTracks[7], FIELD_STRING, "linked_pathtrack_8" ), DEFINE_KEYFIELD( m_iszLinkedCPs[7], FIELD_STRING, "linked_cp_8" ), DEFINE_KEYFIELD( m_bTrainCanRecede, FIELD_BOOLEAN, "train_can_recede" ), DEFINE_KEYFIELD( m_bHandleTrainMovement, FIELD_BOOLEAN, "handle_train_movement" ), // can be up to 8 links // min speed for train hud speed levels DEFINE_KEYFIELD( m_flSpeedLevels[0], FIELD_FLOAT, "hud_min_speed_level_1" ), DEFINE_KEYFIELD( m_flSpeedLevels[1], FIELD_FLOAT, "hud_min_speed_level_2" ), DEFINE_KEYFIELD( m_flSpeedLevels[2], FIELD_FLOAT, "hud_min_speed_level_3" ), DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), DEFINE_KEYFIELD( m_iszSparkName, FIELD_STRING, "env_spark_name" ), DEFINE_KEYFIELD( m_flSpeedForwardModifier, FIELD_FLOAT, "speed_forward_modifier" ), DEFINE_KEYFIELD( m_nTrainRecedeTime, FIELD_INTEGER, "train_recede_time" ), END_DATADESC() IMPLEMENT_SERVERCLASS_ST(CTeamTrainWatcher, DT_TeamTrainWatcher) SendPropFloat( SENDINFO( m_flTotalProgress ), 11, 0, 0.0f, 1.0f ), SendPropInt( SENDINFO( m_iTrainSpeedLevel ), 4 ), SendPropTime( SENDINFO( m_flRecedeTime ) ), SendPropInt( SENDINFO( m_nNumCappers ) ), #ifdef GLOWS_ENABLE SendPropEHandle( SENDINFO( m_hGlowEnt ) ), #endif // GLOWS_ENABLE END_SEND_TABLE() LINK_ENTITY_TO_CLASS( team_train_watcher, CTeamTrainWatcher ); IMPLEMENT_AUTO_LIST( ITFTeamTrainWatcher ); /* LINK_ENTITY_TO_CLASS( team_train_watcher_master, CTeamTrainWatcherMaster ); PRECACHE_REGISTER( team_train_watcher_master ); CTeamTrainWatcherMaster::CTeamTrainWatcherMaster() { m_pBlueWatcher = NULL; m_pRedWatcher = NULL; m_flBlueProgress = 0.0f; m_flRedProgress = 0.0f; ListenForGameEvent( "teamplay_round_start" ); ListenForGameEvent( "teamplay_round_win" ); } CTeamTrainWatcherMaster::~CTeamTrainWatcherMaster() { if ( g_hTeamTrainWatcherMaster.Get() == this ) { g_hTeamTrainWatcherMaster = NULL; } } void CTeamTrainWatcherMaster::Precache( void ) { PrecacheScriptSound( TWM_FIRSTSTAGEOUTCOME01 ); PrecacheScriptSound( TWM_FIRSTSTAGEOUTCOME02 ); PrecacheScriptSound( TWM_RACEGENERAL01 ); PrecacheScriptSound( TWM_RACEGENERAL02 ); PrecacheScriptSound( TWM_RACEGENERAL03 ); PrecacheScriptSound( TWM_RACEGENERAL04 ); PrecacheScriptSound( TWM_RACEGENERAL05 ); PrecacheScriptSound( TWM_RACEGENERAL08 ); PrecacheScriptSound( TWM_RACEGENERAL06 ); PrecacheScriptSound( TWM_RACEGENERAL07 ); PrecacheScriptSound( TWM_RACEGENERAL09 ); PrecacheScriptSound( TWM_RACEGENERAL12 ); PrecacheScriptSound( TWM_RACEGENERAL13 ); PrecacheScriptSound( TWM_RACEGENERAL14 ); PrecacheScriptSound( TWM_RACEGENERAL15 ); PrecacheScriptSound( TWM_RACEGENERAL10 ); PrecacheScriptSound( TWM_RACEGENERAL11 ); PrecacheScriptSound( TWM_SECONDSTAGEOUTCOME01 ); PrecacheScriptSound( TWM_SECONDSTAGEOUTCOME04 ); PrecacheScriptSound( TWM_SECONDSTAGEOUTCOME02 ); PrecacheScriptSound( TWM_SECONDSTAGEOUTCOME03 ); PrecacheScriptSound( TWM_FINALSTAGEOUTCOME01 ); PrecacheScriptSound( TWM_FINALSTAGEOUTCOME02 ); PrecacheScriptSound( TWM_FINALSTAGESTART01 ); PrecacheScriptSound( TWM_FINALSTAGESTART04 ); PrecacheScriptSound( TWM_FINALSTAGESTART08 ); PrecacheScriptSound( TWM_FINALSTAGESTART09 ); PrecacheScriptSound( TWM_FINALSTAGESTART07 ); PrecacheScriptSound( TWM_FINALSTAGESTART02 ); PrecacheScriptSound( TWM_FINALSTAGESTART03 ); PrecacheScriptSound( TWM_FINALSTAGESTART05 ); PrecacheScriptSound( TWM_FINALSTAGESTART06 ); BaseClass::Precache(); } bool CTeamTrainWatcherMaster::FindTrainWatchers( void ) { m_pBlueWatcher = NULL; m_pRedWatcher = NULL; // find the train_watchers for this round CTeamTrainWatcher *pTrainWatcher = (CTeamTrainWatcher *)gEntList.FindEntityByClassname( NULL, "team_train_watcher" ); while ( pTrainWatcher ) { if ( pTrainWatcher->IsDisabled() == false ) { if ( pTrainWatcher->GetTeamNumber() == TF_TEAM_BLUE ) { m_pBlueWatcher = pTrainWatcher; } else if ( pTrainWatcher->GetTeamNumber() == TF_TEAM_RED ) { m_pRedWatcher = pTrainWatcher; } } pTrainWatcher = (CTeamTrainWatcher *)gEntList.FindEntityByClassname( pTrainWatcher, "team_train_watcher" ); } return ( m_pBlueWatcher && m_pRedWatcher ); } void CTeamTrainWatcherMaster::TWMThink( void ) { if ( TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->State_Get() != GR_STATE_RND_RUNNING ) { // the next time we 'think' SetContextThink( &CTeamTrainWatcherMaster::TWMThink, gpGlobals->curtime + 0.2, TWMASTER_THINK ); return; } // the next time we 'think' SetContextThink( &CTeamTrainWatcherMaster::TWMThink, gpGlobals->curtime + 0.2, TWMASTER_THINK ); } void CTeamTrainWatcherMaster::FireGameEvent( IGameEvent *event ) { const char *eventname = event->GetName();` if ( FStrEq( "teamplay_round_start", eventname ) ) { if ( TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->HasMultipleTrains() ) { if ( FindTrainWatchers() ) { // we found train watchers so start thinking SetContextThink( &CTeamTrainWatcherMaster::TWMThink, gpGlobals->curtime + 0.2, TWMASTER_THINK ); } } } else if ( FStrEq( "teamplay_round_win", eventname ) ) { if ( TeamplayRoundBasedRules() ) { int iWinningTeam = event->GetInt( "team" ); int iLosingTeam = ( iWinningTeam == TF_TEAM_RED ) ? TF_TEAM_BLUE : TF_TEAM_RED; bool bFullRound = event->GetBool( "full_round" ); CTeamRecipientFilter filterWinner( iWinningTeam, true ); CTeamRecipientFilter filterLoser( iLosingTeam, true ); if ( bFullRound ) { EmitSound( filterWinner, entindex(), TWM_FINALSTAGEOUTCOME01 ); EmitSound( filterLoser, entindex(), TWM_FINALSTAGEOUTCOME02 ); } else { EmitSound( filterWinner, entindex(), TWM_FIRSTSTAGEOUTCOME01 ); EmitSound( filterLoser, entindex(), TWM_FIRSTSTAGEOUTCOME02 ); } } } } */ CTeamTrainWatcher::CTeamTrainWatcher() { m_bDisabled = false; m_flRecedeTime = 0; m_bWaitingToRecede = false; m_bCapBlocked = false; m_flNextSpeakForwardConceptTime = 0; m_hAreaCap = NULL; m_bTrainCanRecede = true; m_bAlarmPlayed = false; m_pAlarm = NULL; m_flAlarmEndTime = -1; m_bHandleTrainMovement = false; m_flSpeedForwardModifier = 1.0f; m_iCurrentHillType = HILL_TYPE_NONE; m_flCurrentSpeed = 0.0f; m_bReceding = false; m_flTrainDistanceFromStart = 0.0f; m_flTrainDistanceAccumulator = 0.f; m_nTrainRecedeTime = 0; #ifdef GLOWS_ENABLE m_hGlowEnt.Set( NULL ); #endif // GLOWS_ENABLE #ifdef TF_DLL ChangeTeam( TF_TEAM_BLUE ); #else ChangeTeam( TEAM_UNASSIGNED ); #endif /* // create a CTeamTrainWatcherMaster entity if ( g_hTeamTrainWatcherMaster.Get() == NULL ) { g_hTeamTrainWatcherMaster = CreateEntityByName( "team_train_watcher_master" ); } */ ListenForGameEvent( "path_track_passed" ); } CTeamTrainWatcher::~CTeamTrainWatcher() { m_Sparks.Purge(); } void CTeamTrainWatcher::UpdateOnRemove( void ) { StopCaptureAlarm(); BaseClass::UpdateOnRemove(); } int CTeamTrainWatcher::UpdateTransmitState() { if ( m_bDisabled ) { return SetTransmitState( FL_EDICT_DONTSEND ); } return SetTransmitState( FL_EDICT_ALWAYS ); } void CTeamTrainWatcher::InputRoundActivate( inputdata_t &inputdata ) { StopCaptureAlarm(); if ( !m_bDisabled ) { WatcherActivate(); } } void CTeamTrainWatcher::InputEnable( inputdata_t &inputdata ) { StopCaptureAlarm(); m_bDisabled = false; WatcherActivate(); UpdateTransmitState(); } void CTeamTrainWatcher::InputDisable( inputdata_t &inputdata ) { StopCaptureAlarm(); m_bDisabled = true; SetContextThink( NULL, 0, TW_THINK ); m_bWaitingToRecede = false; m_Sparks.Purge(); #ifdef GLOWS_ENABLE m_hGlowEnt.Set( NULL ); #endif // GLOWS_ENABLE // if we're moving the train, let's shut it down if ( m_bHandleTrainMovement ) { m_flCurrentSpeed = 0.0f; if ( m_hTrain ) { m_hTrain->SetSpeedDirAccel( m_flCurrentSpeed ); } // handle the sparks under the train HandleSparks( false ); } UpdateTransmitState(); } ConVar tf_escort_recede_time( "tf_escort_recede_time", "30", 0, "", true, 0, false, 0 ); ConVar tf_escort_recede_time_overtime( "tf_escort_recede_time_overtime", "5", 0, "", true, 0, false, 0 ); void CTeamTrainWatcher::FireGameEvent( IGameEvent *event ) { if ( IsDisabled() || !m_bHandleTrainMovement ) return; const char *pszEventName = event->GetName(); if ( FStrEq( pszEventName, "path_track_passed" ) ) { int iIndex = event->GetInt( "index" ); CPathTrack *pNode = dynamic_cast< CPathTrack* >( UTIL_EntityByIndex( iIndex ) ); if ( pNode ) { bool bHandleEvent = false; CPathTrack *pTempNode = m_hStartNode.Get(); // is this a node in the track we're watching? while ( pTempNode ) { if ( pTempNode == pNode ) { bHandleEvent = true; break; } pTempNode = pTempNode->GetNext(); } if ( bHandleEvent ) { // If we're receding and we've hit a node but the next node (going backwards) is disabled // the train is going to stop (like at the base of a downhill section) when we start forward // again we won't pass this node again so don't change our hill state based on this node. if ( m_bReceding ) { if ( pNode->GetPrevious() && pNode->GetPrevious()->IsDisabled() ) { return; } } int iHillType = pNode->GetHillType(); bool bUpdate = ( m_iCurrentHillType != iHillType ); if ( !bUpdate ) { // the hill settings are the same, but are we leaving an uphill or downhill segment? if ( m_iCurrentHillType != HILL_TYPE_NONE ) { // let's peek at the next node CPathTrack *pNextNode = pNode->GetNext(); if ( m_flCurrentSpeed < 0 ) { // we're going backwards pNextNode = pNode->GetPrevious(); } if ( pNextNode ) { int iNextHillType = pNextNode->GetHillType(); if ( m_iCurrentHillType != iNextHillType ) { // we're leaving an uphill or downhill segment...so reset our state until we pass the next node bUpdate = true; iHillType = HILL_TYPE_NONE; } } } } if ( bUpdate ) { m_iCurrentHillType = iHillType; HandleTrainMovement(); } } } } } void CTeamTrainWatcher::HandleSparks( bool bSparks ) { if ( IsDisabled() || !m_bHandleTrainMovement ) return; for ( int i = 0 ; i < m_Sparks.Count() ; i++ ) { CEnvSpark* pSpark = m_Sparks[i].Get(); if ( pSpark && ( pSpark->IsSparking() != bSparks ) ) { if ( bSparks ) { pSpark->StartSpark(); } else { pSpark->StopSpark(); } } } } void CTeamTrainWatcher::HandleTrainMovement( bool bStartReceding /* = false */ ) { if ( IsDisabled() || !m_bHandleTrainMovement ) return; if ( m_hTrain ) { float flSpeed = 0.0f; if ( bStartReceding ) { flSpeed = -0.1f; m_bReceding = true; } else { // do we have cappers on the train? if ( m_nNumCappers > 0 ) { m_bReceding = false; if ( m_iCurrentHillType == HILL_TYPE_DOWNHILL ) { flSpeed = 1.0f; } else { switch( m_nNumCappers ) { case 1: flSpeed = 0.55f; break; case 2: flSpeed = 0.77f; break; case 3: default: flSpeed = 1.0f; break; } } } else if ( m_nNumCappers == -1 ) { // we'll get a -1 for a blocked cart (speed should be 0 for that unless we're on a hill) if ( m_iCurrentHillType == HILL_TYPE_DOWNHILL ) { flSpeed = 1.0f; } } else { // there's nobody on the train, what should it be doing? if ( m_flCurrentSpeed > 0 ) { if ( m_iCurrentHillType == HILL_TYPE_DOWNHILL ) { flSpeed = 1.0f; } } else { // we're rolling backwards if ( m_iCurrentHillType == HILL_TYPE_UPHILL ) { flSpeed = -1.0f; } else { if ( m_bReceding ) { // resume our previous backup speed flSpeed = -0.1f; } } } } } // only need to update the train if our speed has changed if ( m_flCurrentSpeed != flSpeed ) { if ( flSpeed >= 0.0f ) { m_bReceding = false; } m_flCurrentSpeed = flSpeed; m_hTrain->SetSpeedDirAccel( m_flCurrentSpeed ); // handle the sparks under the train bool bSparks = false; if ( m_flCurrentSpeed < 0 ) { bSparks = true; } HandleSparks( bSparks ); } } } void CTeamTrainWatcher::InputSetSpeedForwardModifier( inputdata_t &inputdata ) { InternalSetSpeedForwardModifier( inputdata.value.Float() ); } void CTeamTrainWatcher::InternalSetSpeedForwardModifier( float flModifier ) { if ( IsDisabled() || !m_bHandleTrainMovement ) return; // store the passed value float flSpeedForwardModifier = flModifier; flSpeedForwardModifier = fabs( flSpeedForwardModifier ); m_flSpeedForwardModifier = clamp( flSpeedForwardModifier, 0.f, 1.f ); if ( m_hTrain ) { m_hTrain->SetSpeedForwardModifier( m_flSpeedForwardModifier ); } } void CTeamTrainWatcher::InternalSetNumTrainCappers( int iNumCappers, CBaseEntity *pTrigger ) { if ( IsDisabled() ) return; m_nNumCappers = iNumCappers; // inputdata.pCaller is hopefully an area capture // lets see if its blocked, and not start receding if it is CTriggerAreaCapture *pAreaCap = dynamic_cast( pTrigger ); if ( pAreaCap ) { m_bCapBlocked = pAreaCap->IsBlocked(); m_hAreaCap = pAreaCap; } if ( iNumCappers <= 0 && !m_bCapBlocked && m_bTrainCanRecede ) { if ( !m_bWaitingToRecede ) { // start receding in [tf_escort_cart_recede_time] seconds m_bWaitingToRecede = true; if ( TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->InOvertime() ) { m_flRecedeTotalTime = tf_escort_recede_time_overtime.GetFloat(); } else { m_flRecedeTotalTime = tf_escort_recede_time.GetFloat(); if ( m_nTrainRecedeTime > 0 ) { m_flRecedeTotalTime = m_nTrainRecedeTime; } } m_flRecedeStartTime = gpGlobals->curtime; m_flRecedeTime = m_flRecedeStartTime + m_flRecedeTotalTime; } } else { // cancel receding m_bWaitingToRecede = false; m_flRecedeTime = 0; } HandleTrainMovement(); } // only used for train watchers that control the train movement void CTeamTrainWatcher::SetNumTrainCappers( int iNumCappers, CBaseEntity *pTrigger ) { if ( IsDisabled() || !m_bHandleTrainMovement ) return; InternalSetNumTrainCappers( iNumCappers, pTrigger ); } void CTeamTrainWatcher::InputSetNumTrainCappers( inputdata_t &inputdata ) { InternalSetNumTrainCappers( inputdata.value.Int(), inputdata.pCaller ); } void CTeamTrainWatcher::InputSetTrainRecedeTime( inputdata_t &inputdata ) { int nSeconds = inputdata.value.Int(); if ( nSeconds >= 0 ) { m_nTrainRecedeTime = nSeconds; } else { m_nTrainRecedeTime = 0; } } void CTeamTrainWatcher::InputSetTrainRecedeTimeAndUpdate(inputdata_t &inputdata) { InputSetTrainRecedeTime( inputdata ); // update our time if we're already counting down if ( m_flRecedeTime > 0 ) { m_flRecedeTotalTime = tf_escort_recede_time.GetFloat(); if ( m_nTrainRecedeTime > 0 ) { m_flRecedeTotalTime = m_nTrainRecedeTime; } m_flRecedeStartTime = gpGlobals->curtime; m_flRecedeTime = m_flRecedeStartTime + m_flRecedeTotalTime; } } void CTeamTrainWatcher::InputSetTrainCanRecede( inputdata_t &inputdata ) { m_bTrainCanRecede = inputdata.value.Bool(); } void CTeamTrainWatcher::InputOnStartOvertime( inputdata_t &inputdata ) { // recalculate the recede time if ( m_bWaitingToRecede ) { float flRecedeTimeRemaining = m_flRecedeTime - gpGlobals->curtime; float flOvertimeRecedeLen = tf_escort_recede_time_overtime.GetFloat(); // drop to overtime recede time if it's more than that if ( flRecedeTimeRemaining > flOvertimeRecedeLen ) { m_flRecedeTotalTime = flOvertimeRecedeLen; m_flRecedeStartTime = gpGlobals->curtime; m_flRecedeTime = m_flRecedeStartTime + m_flRecedeTotalTime; } } } #ifdef GLOWS_ENABLE void CTeamTrainWatcher::FindGlowEntity( void ) { if ( m_hTrain && ( m_hTrain->GetEntityName() != NULL_STRING ) ) { string_t iszTrainName = m_hTrain->GetEntityName(); CBaseEntity *pGlowEnt = NULL; // first try to find a phys_constraint relationship with the train CPhysFixed *pPhysConstraint = dynamic_cast( gEntList.FindEntityByClassname( NULL, "phys_constraint" ) ); while ( pPhysConstraint ) { string_t iszName1 = pPhysConstraint->GetNameAttach1(); string_t iszName2 = pPhysConstraint->GetNameAttach2(); if ( iszTrainName == iszName1 ) { pGlowEnt = gEntList.FindEntityByName( NULL, STRING( iszName2 ) ); break; } else if ( iszTrainName == iszName2 ) { pGlowEnt = gEntList.FindEntityByName( NULL, STRING( iszName1 ) ); break; } pPhysConstraint = dynamic_cast( gEntList.FindEntityByClassname( pPhysConstraint, "phys_constraint" ) ); } if ( !pGlowEnt ) { // if we're here, we haven't found the glow entity yet...try all of the prop_dynamic entities CDynamicProp *pPropDynamic = dynamic_cast( gEntList.FindEntityByClassname( NULL, "prop_dynamic" ) ); while ( pPropDynamic ) { if ( pPropDynamic->GetParent() == m_hTrain ) { pGlowEnt = pPropDynamic; break; } pPropDynamic = dynamic_cast( gEntList.FindEntityByClassname( pPropDynamic, "prop_dynamic" ) ); } } // if we still haven't found a glow entity, just have the CFuncTrackTrain glow if ( !pGlowEnt ) { pGlowEnt = m_hTrain.Get(); } if ( pGlowEnt ) { pGlowEnt->SetTransmitState( FL_EDICT_ALWAYS ); m_hGlowEnt.Set( pGlowEnt ); } } } #endif // GLOWS_ENABLE // ========================================================== // given a start node and a list of goal nodes // calculate the distance between each // ========================================================== void CTeamTrainWatcher::WatcherActivate( void ) { m_flRecedeTime = 0; m_bWaitingToRecede = false; m_bCapBlocked = false; m_flNextSpeakForwardConceptTime = 0; m_hAreaCap = NULL; m_flTrainDistanceFromStart = 0.0f; m_bAlarmPlayed = false; m_Sparks.Purge(); StopCaptureAlarm(); // init our train m_hTrain = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszTrain ) ); if ( !m_hTrain ) { Warning("%s failed to find train named '%s'\n", GetClassname(), STRING( m_iszTrain ) ); } // find the trigger area that will give us movement updates and find the sparks (if we're going to handle the train movement) if ( m_bHandleTrainMovement ) { if ( m_hTrain ) { for ( int i=0; i( ITriggerAreaCaptureAutoList::AutoList()[i] ); if ( pArea->GetParent() == m_hTrain.Get() ) { // this is the capture area we care about, so let it know that we want updates on the capture numbers pArea->SetTrainWatcher( this ); break; } } } // init the sprites (if any) CEnvSpark *pSpark = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszSparkName ) ); while ( pSpark ) { m_Sparks.AddToTail( pSpark ); pSpark = dynamic_cast( gEntList.FindEntityByName( pSpark, m_iszSparkName ) ); } } // init our array of path_tracks linked to control points m_iNumCPLinks = 0; int i; for ( i = 0 ; i < MAX_CONTROL_POINTS ; i++ ) { CPathTrack *pPathTrack = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszLinkedPathTracks[i] ) ); CTeamControlPoint *pCP = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszLinkedCPs[i] ) ); if ( pPathTrack && pCP ) { m_CPLinks[m_iNumCPLinks].hPathTrack = pPathTrack; m_CPLinks[m_iNumCPLinks].hCP = pCP; m_CPLinks[m_iNumCPLinks].flDistanceFromStart = 0; // filled in when we parse the nodes m_CPLinks[m_iNumCPLinks].bAlertPlayed = false; m_iNumCPLinks++; } } // init our start and goal nodes m_hStartNode = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszStartNode ) ); if ( !m_hStartNode ) { Warning("%s failed to find path_track named '%s'\n", GetClassname(), STRING(m_iszStartNode) ); } m_hGoalNode = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszGoalNode ) ); if ( !m_hGoalNode ) { Warning("%s failed to find path_track named '%s'\n", GetClassname(), STRING(m_iszGoalNode) ); } m_flTotalPathDistance = 0.0f; CUtlVector< float > hillData; bool bOnHill = false; bool bDownHillData[TEAM_TRAIN_MAX_HILLS]; Q_memset( bDownHillData, 0, sizeof( bDownHillData ) ); int iHillCount = 0; if( m_hStartNode.Get() && m_hGoalNode.Get() ) { CPathTrack *pNode = m_hStartNode; CPathTrack *pPrev = pNode; CPathTrack *pHillStart = NULL; pNode = pNode->GetNext(); int iHillType = HILL_TYPE_NONE; // don't check the start node for links. If it's linked, it will have 0 distance anyway while ( pNode ) { Vector dir = pNode->GetLocalOrigin() - pPrev->GetLocalOrigin(); float length = dir.Length(); m_flTotalPathDistance += length; // gather our hill data for the HUD if ( pNode->GetHillType() != iHillType ) { if ( !bOnHill ) // we're at the start of a hill { hillData.AddToTail( m_flTotalPathDistance ); bOnHill = true; pHillStart = pNode; if ( iHillCount < TEAM_TRAIN_MAX_HILLS ) { bDownHillData[iHillCount] = pNode->IsDownHill() ? true : false; iHillCount++; } } else // we're at the end of a hill { float flDistance = m_flTotalPathDistance - length; // subtract length because the prev node was the end of the hill (not this one) if ( pHillStart && ( pHillStart == pPrev ) ) { flDistance = m_flTotalPathDistance; // we had a single node marked as a hill, so we'll use the current distance as the next marker } hillData.AddToTail( flDistance ); // is our current node the start of another hill? if ( pNode->GetHillType() != HILL_TYPE_NONE ) { hillData.AddToTail( m_flTotalPathDistance ); bOnHill = true; pHillStart = pNode; if ( iHillCount < TEAM_TRAIN_MAX_HILLS ) { bDownHillData[iHillCount] = pNode->IsDownHill() ? true : false; iHillCount++; } } else { bOnHill = false; pHillStart = NULL; } } iHillType = pNode->GetHillType(); } // if pNode is one of our cp nodes, store its distance from m_hStartNode for ( i = 0 ; i < m_iNumCPLinks ; i++ ) { if ( m_CPLinks[i].hPathTrack == pNode ) { m_CPLinks[i].flDistanceFromStart = m_flTotalPathDistance; break; } } if ( pNode == m_hGoalNode ) break; pPrev = pNode; pNode = pNode->GetNext(); } } // if we don't have an even number of entries in our hill data (beginning/end) add the final distance if ( ( hillData.Count() % 2 ) != 0 ) { hillData.AddToTail( m_flTotalPathDistance ); } if ( ObjectiveResource() ) { ObjectiveResource()->ResetHillData( GetTeamNumber() ); // convert our hill data into 0-1 percentages for networking if ( m_flTotalPathDistance > 0 && hillData.Count() > 0 ) { i = 0; while ( i < hillData.Count() ) { if ( i < TEAM_TRAIN_HILLS_ARRAY_SIZE - 1 ) // - 1 because we want to use 2 entries { // add/subtract to the hill start/end to fix rounding errors in the HUD when the train // stops at the bottom/top of a hill but the HUD thinks the train is still on the hill ObjectiveResource()->SetHillData( GetTeamNumber(), (hillData[i] / m_flTotalPathDistance) + 0.005f, (hillData[i+1] / m_flTotalPathDistance) - 0.005f, bDownHillData[i/2] ); } i = i + 2; } } } // We have total distance and increments in our links array for ( i=0;iGetPointIndex(); // This can be pulled once DoD includes team_objectiveresource.* and c_team_objectiveresource.* #ifndef DOD_DLL ObjectiveResource()->SetTrainPathDistance( iCPIndex, m_CPLinks[i].flDistanceFromStart / m_flTotalPathDistance ); #endif } #ifdef GLOWS_ENABLE FindGlowEntity(); #endif // GLOWS_ENABLE InternalSetSpeedForwardModifier( m_flSpeedForwardModifier ); SetContextThink( &CTeamTrainWatcher::WatcherThink, gpGlobals->curtime + 0.1, TW_THINK ); } void CTeamTrainWatcher::StopCaptureAlarm( void ) { if ( m_pAlarm ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pAlarm ); m_pAlarm = NULL; m_flAlarmEndTime = -1.0f; } SetContextThink( NULL, 0, TW_ALARM_THINK ); } void CTeamTrainWatcher::StartCaptureAlarm( CTeamControlPoint *pPoint ) { StopCaptureAlarm(); if ( pPoint ) { CReliableBroadcastRecipientFilter filter; CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); m_pAlarm = controller.SoundCreate( filter, pPoint->entindex(), CHAN_STATIC, TEAM_TRAIN_ALARM, ATTN_NORM ); controller.Play( m_pAlarm, 1.0, PITCH_NORM ); m_flAlarmEndTime = gpGlobals->curtime + MAX_ALARM_TIME_NO_RECEDE; } } void CTeamTrainWatcher::PlayCaptureAlert( CTeamControlPoint *pPoint, bool bFinalPointInMap ) { if ( !pPoint ) return; if ( TeamplayRoundBasedRules() ) { TeamplayRoundBasedRules()->PlayTrainCaptureAlert( pPoint, bFinalPointInMap ); } } ConVar tf_show_train_path( "tf_show_train_path", "0", FCVAR_CHEAT ); void CTeamTrainWatcher::WatcherThink( void ) { if ( m_bWaitingToRecede ) { if ( m_flRecedeTime < gpGlobals->curtime ) { m_bWaitingToRecede = false; // don't actually recede in overtime if ( TeamplayRoundBasedRules() && !TeamplayRoundBasedRules()->InOvertime() ) { // fire recede output m_OnTrainStartRecede.FireOutput( this, this ); HandleTrainMovement( true ); } } } bool bDisableAlarm = (TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->State_Get() != GR_STATE_RND_RUNNING); if ( bDisableAlarm ) { StopCaptureAlarm(); } // given its next node, we can walk the nodes and find the linear // distance to the next cp node, or to the goal node CFuncTrackTrain *pTrain = m_hTrain; if ( pTrain ) { int iOldTrainSpeedLevel = m_iTrainSpeedLevel; // how fast is the train moving? float flSpeed = pTrain->GetDesiredSpeed(); // divide speed into regions // anything negative is -1 if ( flSpeed < 0 ) { m_iTrainSpeedLevel = -1; // even though our desired speed might be negative, // our actual speed might be zero if we're at a dead end... // this will turn off the < image when the train is done moving backwards if ( pTrain->GetCurrentSpeed() == 0 ) { m_iTrainSpeedLevel = 0; } } else if ( flSpeed > m_flSpeedLevels[2] ) { m_iTrainSpeedLevel = 3; } else if ( flSpeed > m_flSpeedLevels[1] ) { m_iTrainSpeedLevel = 2; } else if ( flSpeed > m_flSpeedLevels[0] ) { m_iTrainSpeedLevel = 1; } else { m_iTrainSpeedLevel = 0; } if ( m_iTrainSpeedLevel != iOldTrainSpeedLevel ) { // make sure the sparks are off if we're not moving backwards anymore if ( m_bHandleTrainMovement ) { if ( m_iTrainSpeedLevel == 0 && iOldTrainSpeedLevel != 0 ) { HandleSparks( false ); } } // play any concepts that we might need to play if ( TeamplayRoundBasedRules() ) { if ( m_iTrainSpeedLevel == 0 && iOldTrainSpeedLevel != 0 ) { TeamplayRoundBasedRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_CART_STOP ); m_flNextSpeakForwardConceptTime = 0; } else if ( m_iTrainSpeedLevel < 0 && iOldTrainSpeedLevel == 0 ) { TeamplayRoundBasedRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_CART_MOVING_BACKWARD ); m_flNextSpeakForwardConceptTime = 0; } } } if ( m_iTrainSpeedLevel > 0 && m_flNextSpeakForwardConceptTime < gpGlobals->curtime ) { if ( m_hAreaCap.Get() ) { for ( int i = 1; i <= gpGlobals->maxClients; i++ ) { CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer( UTIL_PlayerByIndex( i ) ); if ( pPlayer ) { if ( m_hAreaCap->IsTouching( pPlayer ) ) { pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_CART_MOVING_FORWARD ); } } } } m_flNextSpeakForwardConceptTime = gpGlobals->curtime + 3.0; } // what percent progress are we at? CPathTrack *pNode = ( pTrain->m_ppath ) ? pTrain->m_ppath->GetNext() : NULL; // if we're moving backwards, GetNext is going to be wrong if ( flSpeed < 0 ) { pNode = pTrain->m_ppath; } if ( pNode ) { float flDistanceToGoal = 0; // distance to next node Vector vecDir = pNode->GetLocalOrigin() - pTrain->GetLocalOrigin(); flDistanceToGoal = vecDir.Length(); // distance of next node to goal node if ( pNode && pNode != m_hGoalNode ) { // walk this until we get to goal node, or a dead end CPathTrack *pPrev = pNode; pNode = pNode->GetNext(); while ( pNode ) { vecDir = pNode->GetLocalOrigin() - pPrev->GetLocalOrigin(); flDistanceToGoal += vecDir.Length(); if ( pNode == m_hGoalNode ) break; pPrev = pNode; pNode = pNode->GetNext(); } } if ( m_flTotalPathDistance <= 0 ) { Assert( !"No path distance in team_train_watcher\n" ); m_flTotalPathDistance = 1; } m_flTotalProgress = clamp( 1.0 - ( flDistanceToGoal / m_flTotalPathDistance ), 0.0, 1.0 ); float flLastDistanceFromStart = m_flTrainDistanceFromStart; m_flTrainDistanceFromStart = m_flTotalPathDistance - flDistanceToGoal; //engine->Con_NPrintf( 0, "%f", m_flTrainDistanceAccumulator ); const int nMinProgressToReport = 16; // 16 hammer units per foot // Check if we've made forward progress. if ( m_flTrainDistanceFromStart > flLastDistanceFromStart ) { // Accumulate distance traveled m_flTrainDistanceAccumulator += m_flTrainDistanceFromStart - flLastDistanceFromStart; // If we're travelling forward and made enough progress to warrant firing an event if ( m_iTrainSpeedLevel > 0 && m_flTrainDistanceAccumulator >= nMinProgressToReport ) { m_flTrainDistanceAccumulator -= nMinProgressToReport; // Fire an event for all the touching players if ( m_hAreaCap.Get() ) { for ( int i = 1; i <= gpGlobals->maxClients; i++ ) { CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer( UTIL_PlayerByIndex( i ) ); if ( pPlayer ) { if ( m_hAreaCap->IsTouching( pPlayer ) ) { IGameEvent *pEvent = gameeventmanager->CreateEvent( "payload_pushed" ); if ( pEvent ) { pEvent->SetInt( "pusher", pPlayer->GetUserID() ); pEvent->SetInt( "distance", 1 ); gameeventmanager->FireEvent( pEvent, true ); } } } } } } } // play alert sounds if necessary for ( int iCount = 0 ; iCount < m_iNumCPLinks ; iCount++ ) { if ( m_flTrainDistanceFromStart < m_CPLinks[iCount].flDistanceFromStart - TEAM_TRAIN_ALERT_DISTANCE ) { // back up twice the alert distance before resetting our flag to play the warning again if ( ( m_flTrainDistanceFromStart < m_CPLinks[iCount].flDistanceFromStart - ( TEAM_TRAIN_ALERT_DISTANCE * 2 ) ) || // has receded back twice the alert distance or... ( !m_bTrainCanRecede ) ) // used to catch the case where the train doesn't normally recede but has rolled back down a hill away from the CP { // reset our alert flag m_CPLinks[iCount].bAlertPlayed = false; } } else { if ( m_flTrainDistanceFromStart < m_CPLinks[iCount].flDistanceFromStart && !m_CPLinks[iCount].bAlertPlayed ) { m_CPLinks[iCount].bAlertPlayed = true; bool bFinalPointInMap = false; CTeamControlPoint *pCurrentPoint = m_CPLinks[iCount].hCP.Get(); CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; if ( pMaster ) { // if we're not playing mini-rounds if ( !pMaster->PlayingMiniRounds() ) { for ( int i = FIRST_GAME_TEAM ; i < MAX_CONTROL_POINT_TEAMS ; i++ ) { if ( ObjectiveResource() && ObjectiveResource()->TeamCanCapPoint( pCurrentPoint->GetPointIndex(), i ) ) { if ( pMaster->WouldNewCPOwnerWinGame( pCurrentPoint, i ) ) { bFinalPointInMap = true; } } } } else { // or this is the last round if ( pMaster->NumPlayableControlPointRounds() == 1 ) { CTeamControlPointRound *pRound = pMaster->GetCurrentRound(); if ( pRound ) { for ( int i = FIRST_GAME_TEAM ; i < MAX_CONTROL_POINT_TEAMS ; i++ ) { if ( ObjectiveResource() && ObjectiveResource()->TeamCanCapPoint( pCurrentPoint->GetPointIndex(), i ) ) { if ( pRound->WouldNewCPOwnerWinGame( pCurrentPoint, i ) ) { bFinalPointInMap = true; } } } } } } } PlayCaptureAlert( pCurrentPoint, bFinalPointInMap ); } } } // check to see if we need to start or stop the alarm if ( flDistanceToGoal <= TEAM_TRAIN_ALARM_DISTANCE ) { if ( ObjectiveResource() ) { ObjectiveResource()->SetTrackAlarm( GetTeamNumber(), true ); } if ( !bDisableAlarm ) { if ( !m_pAlarm ) { if ( m_iNumCPLinks > 0 && !m_bAlarmPlayed ) { // start the alarm at the final point StartCaptureAlarm( m_CPLinks[m_iNumCPLinks-1].hCP.Get() ); m_bAlarmPlayed = true; // used to prevent the alarm from starting again on maps where the train doesn't recede (alarm loops for short time then only plays singles) } } else { if ( !m_bTrainCanRecede ) // if the train won't recede, we only want to play the alarm for a short time { if ( m_flAlarmEndTime > 0 && m_flAlarmEndTime < gpGlobals->curtime ) { StopCaptureAlarm(); SetContextThink( &CTeamTrainWatcher::WatcherAlarmThink, gpGlobals->curtime + TW_ALARM_THINK_INTERVAL, TW_ALARM_THINK ); } } } } } else { if ( ObjectiveResource() ) { ObjectiveResource()->SetTrackAlarm( GetTeamNumber(), false ); } StopCaptureAlarm(); m_bAlarmPlayed = false; } } if ( tf_show_train_path.GetBool() ) { CPathTrack *nextNode = NULL; CPathTrack *node = m_hStartNode; CPathTrack::BeginIteration(); while( node ) { node->Visit(); nextNode = node->GetNext(); if ( !nextNode || nextNode->HasBeenVisited() ) break; NDebugOverlay::Line( node->GetAbsOrigin(), nextNode->GetAbsOrigin(), 255, 255, 0, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); node = nextNode; } CPathTrack::EndIteration(); // show segment of path train is actually on node = pTrain->m_ppath; if ( node && node->GetNext() ) { NDebugOverlay::HorzArrow( node->GetAbsOrigin(), node->GetNext()->GetAbsOrigin(), 5.0f, 255, 0, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); } } } SetContextThink( &CTeamTrainWatcher::WatcherThink, gpGlobals->curtime + 0.1, TW_THINK ); } void CTeamTrainWatcher::WatcherAlarmThink( void ) { CTeamControlPoint *pPoint = m_CPLinks[m_iNumCPLinks-1].hCP.Get(); if ( pPoint ) { pPoint->EmitSound( TEAM_TRAIN_ALARM_SINGLE ); } SetContextThink( &CTeamTrainWatcher::WatcherAlarmThink, gpGlobals->curtime + TW_ALARM_THINK_INTERVAL, TW_ALARM_THINK ); } CBaseEntity *CTeamTrainWatcher::GetTrainEntity( void ) { return m_hTrain.Get(); } bool CTeamTrainWatcher::TimerMayExpire( void ) { if ( IsDisabled() ) { return true; } // Still in overtime if we're waiting to recede if ( m_bWaitingToRecede ) return false; // capture blocked so we're not receding, but game shouldn't end if ( m_bCapBlocked ) return false; // not waiting, so we're capping, in which case the area capture // will not let us expire return true; } // Project the given position onto the track and return the point and how far along that projected position is void CTeamTrainWatcher::ProjectPointOntoPath( const Vector &pos, Vector *posOnPathResult, float *distanceAlongPathResult ) const { CPathTrack *nextNode = NULL; CPathTrack *node = m_hStartNode; Vector toPos; Vector alongPath; float distanceAlong = 0.0f; Vector closestPointOnPath = vec3_origin; float closestPerpendicularDistanceSq = FLT_MAX; float closestDistanceAlongPath = FLT_MAX; CPathTrack::BeginIteration(); while( node ) { node->Visit(); nextNode = node->GetNext(); if ( !nextNode || nextNode->HasBeenVisited() ) break; alongPath = nextNode->GetAbsOrigin() - node->GetAbsOrigin(); float segmentLength = alongPath.NormalizeInPlace(); toPos = pos - node->GetAbsOrigin(); float segmentOverlap = DotProduct( toPos, alongPath ); if ( segmentOverlap >= 0.0f && segmentOverlap < segmentLength ) { // projection is within segment bounds Vector onPath = node->GetAbsOrigin() + alongPath * segmentOverlap; float perpendicularDistanceSq = ( onPath - pos ).LengthSqr(); if ( perpendicularDistanceSq < closestPerpendicularDistanceSq ) { closestPointOnPath = onPath; closestPerpendicularDistanceSq = perpendicularDistanceSq; closestDistanceAlongPath = distanceAlong + segmentOverlap; } } distanceAlong += segmentLength; node = nextNode; } CPathTrack::EndIteration(); if ( posOnPathResult ) { *posOnPathResult = closestPointOnPath; } if ( distanceAlongPathResult ) { *distanceAlongPathResult = closestDistanceAlongPath; } } // Return true if the given position is farther down the track than the train is bool CTeamTrainWatcher::IsAheadOfTrain( const Vector &pos ) const { float distanceAlongPath; ProjectPointOntoPath( pos, NULL, &distanceAlongPath ); return ( distanceAlongPath > m_flTrainDistanceFromStart ); } // return true if the train is almost at the next checkpoint bool CTeamTrainWatcher::IsTrainNearCheckpoint( void ) const { for( int i = 0; i < m_iNumCPLinks ; ++i ) { if ( m_flTrainDistanceFromStart > m_CPLinks[i].flDistanceFromStart - TEAM_TRAIN_ALERT_DISTANCE && m_flTrainDistanceFromStart < m_CPLinks[i].flDistanceFromStart ) { return true; } } return false; } // return true if the train hasn't left its starting position yet bool CTeamTrainWatcher::IsTrainAtStart( void ) const { return ( m_flTrainDistanceFromStart < TEAM_TRAIN_ALARM_DISTANCE ); } // return world space location of next checkpoint along the path Vector CTeamTrainWatcher::GetNextCheckpointPosition( void ) const { for( int i = 0; i < m_iNumCPLinks ; ++i ) { if ( m_flTrainDistanceFromStart < m_CPLinks[i].flDistanceFromStart ) { return m_CPLinks[i].hPathTrack->GetAbsOrigin(); } } Assert( !"No checkpoint found in team train watcher\n" ); return vec3_origin; } #if defined( STAGING_ONLY ) && defined( TF_DLL ) CON_COMMAND_F( tf_dumptrainstats, "Dump the stats for the current train watcher to the console", FCVAR_GAMEDLL ) { // Listenserver host or rcon access only! if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; CTeamTrainWatcher *pWatcher = NULL; while( ( pWatcher = dynamic_cast< CTeamTrainWatcher * >( gEntList.FindEntityByClassname( pWatcher, "team_train_watcher" ) ) ) != NULL ) { pWatcher->DumpStats(); } } void CTeamTrainWatcher::DumpStats( void ) { float flLastPosition = 0.0f; float flTotalDistance = 0.0f; char szOutput[2048]; char szTemp[256]; V_strcpy_safe( szOutput, "\n\nTrain Watcher stats for team " ); V_strcat_safe( szOutput, ( GetTeamNumber() == TF_TEAM_RED ) ? "Red\n" : "Blue\n" ); for( int i = 0; i < m_iNumCPLinks ; ++i ) { float flDistance = m_CPLinks[i].flDistanceFromStart - flLastPosition; if ( i == 0 ) { V_sprintf_safe( szTemp, "\tControl Point: %d\tDistance from start: %0.2f\n", i + 1, flDistance ); } else { V_sprintf_safe( szTemp, "\tControl Point: %d\tDistance from previous point: %0.2f\n", i + 1, flDistance ); } V_strcat_safe( szOutput, szTemp ); flTotalDistance += flDistance; flLastPosition = m_CPLinks[i].flDistanceFromStart; } V_sprintf_safe( szTemp, "\tTotal Distance: %0.2f\n\n", flTotalDistance ); V_strcat_safe( szOutput, szTemp ); Msg( "%s", szOutput ); } #endif // STAGING_ONLY && TF_DLL