Modified source engine (2017) developed by valve and leaked in 2020. Not for commercial purporses
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1555 lines
42 KiB

//========= 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 ),
// 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_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<CTriggerAreaCapture *>( 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::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<CPhysFixed*>( 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<CPhysFixed*>( 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<CDynamicProp*>( gEntList.FindEntityByClassname( NULL, "prop_dynamic" ) );
while ( pPropDynamic )
{
if ( pPropDynamic->GetParent() == m_hTrain )
{
pGlowEnt = pPropDynamic;
break;
}
pPropDynamic = dynamic_cast<CDynamicProp*>( 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<CFuncTrackTrain*>( 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().Count(); ++i )
{
CTriggerAreaCapture *pArea = static_cast< CTriggerAreaCapture * >( 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<CEnvSpark*>( gEntList.FindEntityByName( NULL, m_iszSparkName ) );
while ( pSpark )
{
m_Sparks.AddToTail( pSpark );
pSpark = dynamic_cast<CEnvSpark*>( 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<CPathTrack*>( gEntList.FindEntityByName( NULL, m_iszLinkedPathTracks[i] ) );
CTeamControlPoint *pCP = dynamic_cast<CTeamControlPoint*>( 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<CPathTrack*>( 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<CPathTrack*>( 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;i<m_iNumCPLinks;i++ )
{
int iCPIndex = m_CPLinks[i].hCP.Get()->GetPointIndex();
// 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 );
m_flTrainDistanceFromStart = m_flTotalPathDistance - flDistanceToGoal;
// 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