//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: Spawn, think, and touch functions for trains, etc. // //=============================================================================// #include "cbase.h" #include "ai_basenpc.h" #include "trains.h" #include "ndebugoverlay.h" #include "entitylist.h" #include "engine/IEngineSound.h" #include "soundenvelope.h" #include "physics_npc_solver.h" #include "vphysics/friction.h" #include "hierarchy.h" #include "vscript_shared.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" static void PlatSpawnInsideTrigger(edict_t *pevPlatform); #define SF_PLAT_TOGGLE 0x0001 class CBasePlatTrain : public CBaseToggle { DECLARE_CLASS( CBasePlatTrain, CBaseToggle ); public: ~CBasePlatTrain(); bool KeyValue( const char *szKeyName, const char *szValue ); void Precache( void ); // This is done to fix spawn flag collisions between this class and a derived class virtual bool IsTogglePlat( void ) { return (m_spawnflags & SF_PLAT_TOGGLE) ? true : false; } DECLARE_DATADESC(); void PlayMovingSound(); void StopMovingSound(); string_t m_NoiseMoving; // sound a plat makes while moving string_t m_NoiseArrived; CSoundPatch *m_pMovementSound; float m_volume; // Sound volume float m_flTWidth; float m_flTLength; }; BEGIN_DATADESC( CBasePlatTrain ) DEFINE_KEYFIELD( m_NoiseMoving, FIELD_SOUNDNAME, "noise1" ), DEFINE_KEYFIELD( m_NoiseArrived, FIELD_SOUNDNAME, "noise2" ), DEFINE_SOUNDPATCH( m_pMovementSound ), DEFINE_KEYFIELD( m_volume, FIELD_FLOAT, "volume" ), DEFINE_FIELD( m_flTWidth, FIELD_FLOAT ), DEFINE_FIELD( m_flTLength, FIELD_FLOAT ), DEFINE_KEYFIELD( m_flLip, FIELD_FLOAT, "lip" ), DEFINE_KEYFIELD( m_flWait, FIELD_FLOAT, "wait" ), DEFINE_KEYFIELD( m_flHeight, FIELD_FLOAT, "height" ), END_DATADESC() bool CBasePlatTrain::KeyValue( const char *szKeyName, const char *szValue ) { if (FStrEq(szKeyName, "rotation")) { m_vecFinalAngle.x = atof(szValue); } else { return BaseClass::KeyValue( szKeyName, szValue ); } return true; } CBasePlatTrain::~CBasePlatTrain() { StopMovingSound(); } void CBasePlatTrain::PlayMovingSound() { StopMovingSound(); if(m_NoiseMoving != NULL_STRING ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); CPASAttenuationFilter filter( this ); m_pMovementSound = controller.SoundCreate( filter, entindex(), CHAN_STATIC, STRING(m_NoiseMoving), ATTN_NORM ); controller.Play( m_pMovementSound, m_volume, PITCH_NORM ); } } void CBasePlatTrain::StopMovingSound() { if ( m_pMovementSound ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.SoundDestroy( m_pMovementSound ); m_pMovementSound = NULL; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBasePlatTrain::Precache( void ) { //Fill in a default value if necessary UTIL_ValidateSoundName( m_NoiseMoving, "Plat.DefaultMoving" ); UTIL_ValidateSoundName( m_NoiseArrived, "Plat.DefaultArrive" ); //Precache them all PrecacheScriptSound( (char *) STRING(m_NoiseMoving) ); PrecacheScriptSound( (char *) STRING(m_NoiseArrived) ); } class CFuncPlat : public CBasePlatTrain { DECLARE_CLASS( CFuncPlat, CBasePlatTrain ); public: void Spawn( void ); void Precache( void ); bool CreateVPhysics(); void Setup( void ); virtual void Blocked( CBaseEntity *pOther ); void PlatUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); void CallGoDown( void ) { GoDown(); } void CallHitTop( void ) { HitTop(); } void CallHitBottom( void ) { HitBottom(); } virtual void GoUp( void ); virtual void GoDown( void ); virtual void HitTop( void ); virtual void HitBottom( void ); void InputToggle(inputdata_t &data); void InputGoUp(inputdata_t &data); void InputGoDown(inputdata_t &data); DECLARE_DATADESC(); private: string_t m_sNoise; }; BEGIN_DATADESC( CFuncPlat ) DEFINE_FIELD( m_sNoise, FIELD_STRING ), // Function Pointers DEFINE_FUNCTION( PlatUse ), DEFINE_FUNCTION( CallGoDown ), DEFINE_FUNCTION( CallHitTop ), DEFINE_FUNCTION( CallHitBottom ), // Inputs DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), DEFINE_INPUTFUNC( FIELD_VOID, "GoUp", InputGoUp ), DEFINE_INPUTFUNC( FIELD_VOID, "GoDown", InputGoDown ), END_DATADESC() LINK_ENTITY_TO_CLASS( func_plat, CFuncPlat ); //================================================== // CPlatTrigger //================================================== class CPlatTrigger : public CBaseEntity { DECLARE_CLASS( CPlatTrigger, CBaseEntity ); public: virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() | FCAP_DONT_SAVE; } void SpawnInsideTrigger( CFuncPlat *pPlatform ); void Touch( CBaseEntity *pOther ); CFuncPlat *m_pPlatform; }; void CFuncPlat::Setup( void ) { if (m_flTLength == 0) { m_flTLength = 80; } if (m_flTWidth == 0) { m_flTWidth = 10; } SetLocalAngles( vec3_angle ); SetSolid( SOLID_BSP ); SetMoveType( MOVETYPE_PUSH ); // Set size and link into world SetModel( STRING( GetModelName() ) ); m_vecPosition1 = GetLocalOrigin(); //Top m_vecPosition2 = GetLocalOrigin(); //Bottom if ( m_flHeight != 0 ) { m_vecPosition2.z = GetLocalOrigin().z - m_flHeight; } else { // NOTE: This works because the angles were set to vec3_angle above m_vecPosition2.z = GetLocalOrigin().z - CollisionProp()->OBBSize().z + 8; } if (m_flSpeed == 0) { m_flSpeed = 150; } if ( m_volume == 0.0f ) { m_volume = 0.85f; } } void CFuncPlat::Precache( ) { BaseClass::Precache(); if ( IsTogglePlat() == false ) { // Create the "start moving" trigger PlatSpawnInsideTrigger( edict() ); } } void CFuncPlat::Spawn( ) { Setup(); Precache(); // If this platform is the target of some button, it starts at the TOP position, // and is brought down by that button. Otherwise, it starts at BOTTOM. if ( GetEntityName() != NULL_STRING ) { UTIL_SetOrigin( this, m_vecPosition1); m_toggle_state = TS_AT_TOP; SetUse( &CFuncPlat::PlatUse ); } else { UTIL_SetOrigin( this, m_vecPosition2); m_toggle_state = TS_AT_BOTTOM; } CreateVPhysics(); } bool CFuncPlat::CreateVPhysics() { VPhysicsInitShadow( false, false ); return true; } static void PlatSpawnInsideTrigger(edict_t* pevPlatform) { // old code: //GetClassPtr( (CPlatTrigger *)NULL)->SpawnInsideTrigger( GetClassPtr( (CFuncPlat *)pevPlatform ) ); CPlatTrigger *plattrig = CREATE_UNSAVED_ENTITY( CPlatTrigger, "plat_trigger" ); plattrig->SpawnInsideTrigger( (CFuncPlat *)GetContainingEntity( pevPlatform ) ); } // // Create a trigger entity for a platform. // void CPlatTrigger::SpawnInsideTrigger( CFuncPlat *pPlatform ) { m_pPlatform = pPlatform; // Create trigger entity, "point" it at the owning platform, give it a touch method SetSolid( SOLID_BSP ); AddSolidFlags( FSOLID_TRIGGER ); SetMoveType( MOVETYPE_NONE ); SetLocalOrigin( pPlatform->GetLocalOrigin() ); // Establish the trigger field's size CCollisionProperty *pCollision = m_pPlatform->CollisionProp(); Vector vecTMin = pCollision->OBBMins() + Vector ( 25 , 25 , 0 ); Vector vecTMax = pCollision->OBBMaxs() + Vector ( 25 , 25 , 8 ); vecTMin.z = vecTMax.z - ( m_pPlatform->m_vecPosition1.z - m_pPlatform->m_vecPosition2.z + 8 ); if ( pCollision->OBBSize().x <= 50 ) { vecTMin.x = (pCollision->OBBMins().x + pCollision->OBBMaxs().x) / 2; vecTMax.x = vecTMin.x + 1; } if ( pCollision->OBBSize().y <= 50 ) { vecTMin.y = (pCollision->OBBMins().y + pCollision->OBBMaxs().y) / 2; vecTMax.y = vecTMin.y + 1; } UTIL_SetSize ( this, vecTMin, vecTMax ); } // // When the platform's trigger field is touched, the platform ??? // void CPlatTrigger::Touch( CBaseEntity *pOther ) { // Ignore touches by non-players if ( !pOther->IsPlayer() ) return; // Ignore touches by corpses if (!pOther->IsAlive()) return; // Make linked platform go up/down. if (m_pPlatform->m_toggle_state == TS_AT_BOTTOM) m_pPlatform->GoUp(); else if (m_pPlatform->m_toggle_state == TS_AT_TOP) m_pPlatform->SetMoveDoneTime( 1 );// delay going down } //----------------------------------------------------------------------------- // Purpose: Used when a platform is the target of a button. // Start bringing platform down. // Input : pActivator - // pCaller - // useType - // value - //----------------------------------------------------------------------------- void CFuncPlat::InputToggle(inputdata_t &data) { if ( IsTogglePlat() ) { if (m_toggle_state == TS_AT_TOP) GoDown(); else if ( m_toggle_state == TS_AT_BOTTOM ) GoUp(); } else { SetUse( NULL ); if (m_toggle_state == TS_AT_TOP) GoDown(); } } void CFuncPlat::InputGoUp(inputdata_t &data) { if ( m_toggle_state == TS_AT_BOTTOM ) GoUp(); } void CFuncPlat::InputGoDown(inputdata_t &data) { if ( m_toggle_state == TS_AT_TOP ) GoDown(); } //----------------------------------------------------------------------------- // Purpose: Used when a platform is the target of a button. // Start bringing platform down. // Input : pActivator - // pCaller - // useType - // value - //----------------------------------------------------------------------------- void CFuncPlat::PlatUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { if ( IsTogglePlat() ) { // Top is off, bottom is on bool on = (m_toggle_state == TS_AT_BOTTOM) ? true : false; if ( !ShouldToggle( useType, on ) ) return; if (m_toggle_state == TS_AT_TOP) GoDown(); else if ( m_toggle_state == TS_AT_BOTTOM ) GoUp(); } else { SetUse( NULL ); if (m_toggle_state == TS_AT_TOP) GoDown(); } } // // Platform is at top, now starts moving down. // void CFuncPlat::GoDown( void ) { PlayMovingSound(); ASSERT(m_toggle_state == TS_AT_TOP || m_toggle_state == TS_GOING_UP); m_toggle_state = TS_GOING_DOWN; SetMoveDone(&CFuncPlat::CallHitBottom); LinearMove(m_vecPosition2, m_flSpeed); } // // Platform has hit bottom. Stops and waits forever. // void CFuncPlat::HitBottom( void ) { StopMovingSound(); if ( m_NoiseArrived != NULL_STRING ) { CPASAttenuationFilter filter( this ); EmitSound_t ep; ep.m_nChannel = CHAN_WEAPON; ep.m_pSoundName = STRING(m_NoiseArrived); ep.m_flVolume = m_volume; ep.m_SoundLevel = SNDLVL_NORM; EmitSound( filter, entindex(), ep ); } ASSERT(m_toggle_state == TS_GOING_DOWN); m_toggle_state = TS_AT_BOTTOM; } // // Platform is at bottom, now starts moving up // void CFuncPlat::GoUp( void ) { PlayMovingSound(); ASSERT(m_toggle_state == TS_AT_BOTTOM || m_toggle_state == TS_GOING_DOWN); m_toggle_state = TS_GOING_UP; SetMoveDone(&CFuncPlat::CallHitTop); LinearMove(m_vecPosition1, m_flSpeed); } // // Platform has hit top. Pauses, then starts back down again. // void CFuncPlat::HitTop( void ) { StopMovingSound(); if ( m_NoiseArrived != NULL_STRING ) { CPASAttenuationFilter filter( this ); EmitSound_t ep; ep.m_nChannel = CHAN_WEAPON; ep.m_pSoundName = STRING(m_NoiseArrived); ep.m_flVolume = m_volume; ep.m_SoundLevel = SNDLVL_NORM; EmitSound( filter, entindex(), ep ); } ASSERT(m_toggle_state == TS_GOING_UP); m_toggle_state = TS_AT_TOP; if ( !IsTogglePlat() ) { // After a delay, the platform will automatically start going down again. SetMoveDone( &CFuncPlat::CallGoDown ); SetMoveDoneTime( 3 ); } } //----------------------------------------------------------------------------- // Purpose: Called when we are blocked. //----------------------------------------------------------------------------- void CFuncPlat::Blocked( CBaseEntity *pOther ) { DevMsg( 2, "%s Blocked by %s\n", GetClassname(), pOther->GetClassname() ); // Hurt the blocker a little pOther->TakeDamage( CTakeDamageInfo( this, this, 1, DMG_CRUSH ) ); if (m_sNoise != NULL_STRING) { StopSound(entindex(), CHAN_STATIC, (char*)STRING(m_sNoise)); } // Send the platform back where it came from ASSERT(m_toggle_state == TS_GOING_UP || m_toggle_state == TS_GOING_DOWN); if (m_toggle_state == TS_GOING_UP) { GoDown(); } else if (m_toggle_state == TS_GOING_DOWN) { GoUp (); } } class CFuncPlatRot : public CFuncPlat { DECLARE_CLASS( CFuncPlatRot, CFuncPlat ); public: void Spawn( void ); void SetupRotation( void ); virtual void GoUp( void ); virtual void GoDown( void ); virtual void HitTop( void ); virtual void HitBottom( void ); void RotMove( QAngle &destAngle, float time ); DECLARE_DATADESC(); QAngle m_end, m_start; }; LINK_ENTITY_TO_CLASS( func_platrot, CFuncPlatRot ); BEGIN_DATADESC( CFuncPlatRot ) DEFINE_FIELD( m_end, FIELD_VECTOR ), DEFINE_FIELD( m_start, FIELD_VECTOR ), END_DATADESC() void CFuncPlatRot::SetupRotation( void ) { if ( m_vecFinalAngle.x != 0 ) // This plat rotates too! { CBaseToggle::AxisDir(); m_start = GetLocalAngles(); m_end = GetLocalAngles() + m_vecMoveAng * m_vecFinalAngle.x; } else { m_start = vec3_angle; m_end = vec3_angle; } if ( GetEntityName() != NULL_STRING ) // Start at top { SetLocalAngles( m_end ); } } void CFuncPlatRot::Spawn( void ) { BaseClass::Spawn(); SetupRotation(); } void CFuncPlatRot::GoDown( void ) { BaseClass::GoDown(); RotMove( m_start, GetMoveDoneTime() ); } // // Platform has hit bottom. Stops and waits forever. // void CFuncPlatRot::HitBottom( void ) { BaseClass::HitBottom(); SetLocalAngularVelocity( vec3_angle ); SetLocalAngles( m_start ); } // // Platform is at bottom, now starts moving up // void CFuncPlatRot::GoUp( void ) { BaseClass::GoUp(); RotMove( m_end, GetMoveDoneTime() ); } // // Platform has hit top. Pauses, then starts back down again. // void CFuncPlatRot::HitTop( void ) { BaseClass::HitTop(); SetLocalAngularVelocity( vec3_angle ); SetLocalAngles( m_end ); } void CFuncPlatRot::RotMove( QAngle &destAngle, float time ) { // set destdelta to the vector needed to move QAngle vecDestDelta = destAngle - GetLocalAngles(); // Travel time is so short, we're practically there already; so make it so. if ( time >= 0.1) SetLocalAngularVelocity( vecDestDelta * (1.0 / time) ); else { SetLocalAngularVelocity( vecDestDelta ); SetMoveDoneTime( 1 ); } } class CFuncTrain : public CBasePlatTrain { DECLARE_CLASS( CFuncTrain, CBasePlatTrain ); public: void Spawn( void ); void Precache( void ); void Activate( void ); void OnRestore( void ); void SetupTarget( void ); void Blocked( CBaseEntity *pOther ); void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); void Wait( void ); void Next( void ); //Inputs void InputToggle(inputdata_t &data); void InputStart(inputdata_t &data); void InputStop(inputdata_t &data); void Start( void ); void Stop( void ); DECLARE_DATADESC(); public: EHANDLE m_hCurrentTarget; bool m_activated; EHANDLE m_hEnemy; float m_flBlockDamage; // Damage to inflict when blocked. float m_flNextBlockTime; string_t m_iszLastTarget; }; LINK_ENTITY_TO_CLASS( func_train, CFuncTrain ); BEGIN_DATADESC( CFuncTrain ) DEFINE_FIELD( m_hCurrentTarget, FIELD_EHANDLE ), DEFINE_FIELD( m_activated, FIELD_BOOLEAN ), DEFINE_FIELD( m_hEnemy, FIELD_EHANDLE ), DEFINE_FIELD( m_iszLastTarget, FIELD_STRING ), DEFINE_FIELD( m_flNextBlockTime, FIELD_TIME ), DEFINE_KEYFIELD( m_flBlockDamage, FIELD_FLOAT, "dmg" ), // Function Pointers DEFINE_FUNCTION( Wait ), DEFINE_FUNCTION( Next ), // Inputs DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), DEFINE_INPUTFUNC( FIELD_VOID, "Start", InputStart ), DEFINE_INPUTFUNC( FIELD_VOID, "Stop", InputStop ), END_DATADESC() //----------------------------------------------------------------------------- // Purpose: Handles a train being blocked by an entity. // Input : pOther - What was hit. //----------------------------------------------------------------------------- void CFuncTrain::Blocked( CBaseEntity *pOther ) { if ( gpGlobals->curtime < m_flNextBlockTime ) return; m_flNextBlockTime = gpGlobals->curtime + 0.5; //Inflict damage pOther->TakeDamage( CTakeDamageInfo( this, this, m_flBlockDamage, DMG_CRUSH ) ); } void CFuncTrain::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { //If we've been waiting to be retriggered, move to the next destination if ( m_spawnflags & SF_TRAIN_WAIT_RETRIGGER ) { // Move toward my target m_spawnflags &= ~SF_TRAIN_WAIT_RETRIGGER; Next(); } else { m_spawnflags |= SF_TRAIN_WAIT_RETRIGGER; // Pop back to last target if it's available if ( m_hEnemy ) { m_target = m_hEnemy->GetEntityName(); } SetNextThink( TICK_NEVER_THINK ); SetLocalVelocity( vec3_origin ); if ( m_NoiseArrived != NULL_STRING ) { CPASAttenuationFilter filter( this ); EmitSound_t ep; ep.m_nChannel = CHAN_VOICE; ep.m_pSoundName = STRING(m_NoiseArrived); ep.m_flVolume = m_volume; ep.m_SoundLevel = SNDLVL_NORM; EmitSound( filter, entindex(), ep ); } } } void CFuncTrain::Wait( void ) { //If we're moving passed a path track, then trip its output variant_t emptyVariant; m_hCurrentTarget->AcceptInput( "InPass", this, this, emptyVariant, 0 ); // need pointer to LAST target. if ( m_hCurrentTarget->HasSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ) || HasSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ) ) { AddSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ); // Clear the sound channel. StopMovingSound(); if ( m_NoiseArrived != NULL_STRING ) { CPASAttenuationFilter filter( this ); EmitSound_t ep; ep.m_nChannel = CHAN_VOICE; ep.m_pSoundName = STRING(m_NoiseArrived); ep.m_flVolume = m_volume; ep.m_SoundLevel = SNDLVL_NORM; EmitSound( filter, entindex(), ep ); } SetMoveDoneTime( -1 ); return; } //NOTENOTE: -1 wait will wait forever if ( m_flWait != 0 ) { SetMoveDoneTime( m_flWait ); StopMovingSound(); if ( m_NoiseArrived != NULL_STRING ) { CPASAttenuationFilter filter( this ); EmitSound_t ep; ep.m_nChannel = CHAN_VOICE; ep.m_pSoundName = STRING(m_NoiseArrived); ep.m_flVolume = m_volume; ep.m_SoundLevel = SNDLVL_NORM; EmitSound( filter, entindex(), ep ); } SetMoveDone( &CFuncTrain::Next ); } else { // Do it right now Next(); } } //----------------------------------------------------------------------------- // Purpose: Advances the train to the next path corner on the path. //----------------------------------------------------------------------------- void CFuncTrain::Next( void ) { //Find our next target CBaseEntity *pTarg = GetNextTarget(); //If none, we're done if ( pTarg == NULL ) { //Stop the moving sound StopMovingSound(); // Play stop sound if ( m_NoiseArrived != NULL_STRING ) { CPASAttenuationFilter filter( this ); EmitSound_t ep; ep.m_nChannel = CHAN_VOICE; ep.m_pSoundName = STRING(m_NoiseArrived); ep.m_flVolume = m_volume; ep.m_SoundLevel = SNDLVL_NORM; EmitSound( filter, entindex(), ep ); } return; } // Save last target in case we need to find it again m_iszLastTarget = m_target; m_target = pTarg->m_target; m_flWait = pTarg->GetDelay(); // If our target has a speed, take it if ( m_hCurrentTarget && m_hCurrentTarget->m_flSpeed != 0 ) { m_flSpeed = m_hCurrentTarget->m_flSpeed; DevMsg( 2, "Train %s speed to %4.2f\n", GetDebugName(), m_flSpeed ); } // Keep track of this since path corners change our target for us m_hCurrentTarget = pTarg; m_hEnemy = pTarg; //Check for teleport if ( m_hCurrentTarget->HasSpawnFlags( SF_CORNER_TELEPORT ) ) { AddEffects( EF_NOINTERP ); // This is supposed to place the center of the func_train at the target's origin. // FIXME: This is totally busted! It's using the wrong space for the computation... UTIL_SetOrigin( this, pTarg->GetLocalOrigin() - CollisionProp()->OBBCenter() ); // Get on with doing the next path corner. Wait(); } else { // Normal linear move PlayMovingSound(); RemoveEffects( EF_NOINTERP ); SetMoveDone( &CFuncTrain::Wait ); // This is supposed to place the center of the func_train at the target's origin. // FIXME: This is totally busted! It's using the wrong space for the computation... LinearMove ( pTarg->GetLocalOrigin() - CollisionProp()->OBBCenter(), m_flSpeed ); } } //----------------------------------------------------------------------------- // Purpose: Called after all the entities spawn. //----------------------------------------------------------------------------- void CFuncTrain::Activate( void ) { BaseClass::Activate(); // Not yet active, so teleport to first target if ( m_activated == false ) { SetupTarget(); m_activated = true; if ( m_hCurrentTarget.Get() == NULL ) return; // This is supposed to place the center of the func_train at the target's origin. // FIXME: This is totally busted! It's using the wrong space for the computation... UTIL_SetOrigin( this, m_hCurrentTarget->GetLocalOrigin() - CollisionProp()->OBBCenter() ); if ( GetSolid() == SOLID_BSP ) { VPhysicsInitShadow( false, false ); } // Start immediately if not triggered if ( !GetEntityName() ) { SetMoveDoneTime( 0.1 ); SetMoveDone( &CFuncTrain::Next ); } else { m_spawnflags |= SF_TRAIN_WAIT_RETRIGGER; } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CFuncTrain::SetupTarget( void ) { // Find our target whenever we don't have one (level transition) if ( !m_hCurrentTarget ) { CBaseEntity *pTarg = gEntList.FindEntityByName( NULL, m_target ); if ( pTarg == NULL ) { Msg( "Can't find target of train %s\n", STRING(m_target) ); return; } // Keep track of this since path corners change our target for us m_target = pTarg->m_target; m_hCurrentTarget = pTarg; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CFuncTrain::Spawn( void ) { Precache(); if ( m_flSpeed == 0 ) { m_flSpeed = 100; } if ( !m_target ) { Warning("FuncTrain '%s' has no target.\n", GetDebugName()); } if ( m_flBlockDamage == 0 ) { m_flBlockDamage = 2; } SetMoveType( MOVETYPE_PUSH ); SetSolid( SOLID_BSP ); SetModel( STRING( GetModelName() ) ); if ( m_spawnflags & SF_TRACKTRAIN_PASSABLE ) { AddSolidFlags( FSOLID_NOT_SOLID ); } m_activated = false; if ( m_volume == 0.0f ) { m_volume = 0.85f; } } void CFuncTrain::Precache( void ) { BaseClass::Precache(); } void CFuncTrain::OnRestore( void ) { BaseClass::OnRestore(); // Are we moving? if ( IsMoving() ) { // Continue moving to the same target m_target = m_iszLastTarget; } SetupTarget(); } void CFuncTrain::InputToggle( inputdata_t &data ) { //If we've been waiting to be retriggered, move to the next destination if( HasSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ) ) { Start(); } else { Stop(); } } void CFuncTrain::InputStart( inputdata_t &data ) { Start(); } void CFuncTrain::InputStop( inputdata_t &data ) { Stop(); } void CFuncTrain::Start( void ) { //start moving if( HasSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ) ) { // Move toward my target RemoveSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ); Next(); } } void CFuncTrain::Stop( void ) { //stop moving if( !HasSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ) ) { AddSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ); // Pop back to last target if it's available if ( m_hEnemy ) { m_target = m_hEnemy->GetEntityName(); } SetNextThink( TICK_NEVER_THINK ); SetAbsVelocity( vec3_origin ); if ( m_NoiseArrived != NULL_STRING ) { CPASAttenuationFilter filter( this ); EmitSound_t ep; ep.m_nChannel = CHAN_VOICE; ep.m_pSoundName = STRING(m_NoiseArrived); ep.m_flVolume = m_volume; ep.m_SoundLevel = SNDLVL_NORM; EmitSound( filter, entindex(), ep ); } //Do not teleport to our final move destination SetMoveDone( NULL ); SetMoveDoneTime( -1 ); } } BEGIN_DATADESC( CFuncTrackTrain ) DEFINE_KEYFIELD( m_length, FIELD_FLOAT, "wheels" ), DEFINE_KEYFIELD( m_height, FIELD_FLOAT, "height" ), DEFINE_KEYFIELD( m_maxSpeed, FIELD_FLOAT, "startspeed" ), DEFINE_KEYFIELD( m_flBank, FIELD_FLOAT, "bank" ), DEFINE_KEYFIELD( m_flBlockDamage, FIELD_FLOAT, "dmg" ), DEFINE_KEYFIELD( m_iszSoundMove, FIELD_SOUNDNAME, "MoveSound" ), DEFINE_KEYFIELD( m_iszSoundMovePing, FIELD_SOUNDNAME, "MovePingSound" ), DEFINE_KEYFIELD( m_iszSoundStart, FIELD_SOUNDNAME, "StartSound" ), DEFINE_KEYFIELD( m_iszSoundStop, FIELD_SOUNDNAME, "StopSound" ), DEFINE_KEYFIELD( m_nMoveSoundMinPitch, FIELD_INTEGER, "MoveSoundMinPitch" ), DEFINE_KEYFIELD( m_nMoveSoundMaxPitch, FIELD_INTEGER, "MoveSoundMaxPitch" ), DEFINE_KEYFIELD( m_flMoveSoundMinTime, FIELD_FLOAT, "MoveSoundMinTime" ), DEFINE_KEYFIELD( m_flMoveSoundMaxTime, FIELD_FLOAT, "MoveSoundMaxTime" ), DEFINE_FIELD( m_flNextMoveSoundTime, FIELD_TIME ), DEFINE_KEYFIELD( m_eVelocityType, FIELD_INTEGER, "velocitytype" ), DEFINE_KEYFIELD( m_eOrientationType, FIELD_INTEGER, "orientationtype" ), DEFINE_FIELD( m_ppath, FIELD_CLASSPTR ), DEFINE_FIELD( m_dir, FIELD_FLOAT ), DEFINE_FIELD( m_controlMins, FIELD_VECTOR ), DEFINE_FIELD( m_controlMaxs, FIELD_VECTOR ), DEFINE_FIELD( m_flVolume, FIELD_FLOAT ), DEFINE_FIELD( m_oldSpeed, FIELD_FLOAT ), DEFINE_FIELD( m_strPathTarget, FIELD_STRING ), //DEFINE_FIELD( m_lastBlockPos, FIELD_POSITION_VECTOR ), // temp values for blocking, don't save //DEFINE_FIELD( m_lastBlockTick, FIELD_INTEGER ), DEFINE_FIELD( m_bSoundPlaying, FIELD_BOOLEAN ), DEFINE_KEYFIELD( m_bManualSpeedChanges, FIELD_BOOLEAN, "ManualSpeedChanges" ), DEFINE_KEYFIELD( m_flAccelSpeed, FIELD_FLOAT, "ManualAccelSpeed" ), DEFINE_KEYFIELD( m_flDecelSpeed, FIELD_FLOAT, "ManualDecelSpeed" ), // Inputs DEFINE_INPUTFUNC( FIELD_VOID, "Stop", InputStop ), DEFINE_INPUTFUNC( FIELD_VOID, "StartForward", InputStartForward ), DEFINE_INPUTFUNC( FIELD_VOID, "StartBackward", InputStartBackward ), DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), DEFINE_INPUTFUNC( FIELD_VOID, "Resume", InputResume ), DEFINE_INPUTFUNC( FIELD_VOID, "Reverse", InputReverse ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeed", InputSetSpeed ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeedDir", InputSetSpeedDir ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeedReal", InputSetSpeedReal ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetMaxSpeed", InputSetMaxSpeed ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeedDirAccel", InputSetSpeedDirAccel ), DEFINE_INPUTFUNC( FIELD_STRING, "MoveToPathNode", InputMoveToPathNode ), DEFINE_INPUTFUNC( FIELD_STRING, "TeleportToPathNode", InputTeleportToPathNode ), DEFINE_INPUTFUNC( FIELD_VOID, "LockOrientation", InputLockOrientation ), DEFINE_INPUTFUNC( FIELD_VOID, "UnlockOrientation", InputUnlockOrientation ), // Outputs DEFINE_OUTPUT( m_OnStart, "OnStart" ), DEFINE_OUTPUT( m_OnNext, "OnNextPoint" ), DEFINE_OUTPUT( m_OnArrivedAtDestinationNode, "OnArrivedAtDestinationNode" ), // Function Pointers DEFINE_FUNCTION( Next ), DEFINE_FUNCTION( Find ), DEFINE_FUNCTION( NearestPath ), DEFINE_FUNCTION( DeadEnd ), END_DATADESC() BEGIN_ENT_SCRIPTDESC( CFuncTrackTrain, CBaseEntity, "func_train" ) DEFINE_SCRIPTFUNC_NAMED( ScriptGetFuturePosition, "GetFuturePosition", "Get a position on the track x seconds in the future" ) END_SCRIPTDESC() LINK_ENTITY_TO_CLASS( func_tracktrain, CFuncTrackTrain ); //----------------------------------------------------------------------------- // Datatable //----------------------------------------------------------------------------- IMPLEMENT_SERVERCLASS_ST( CFuncTrackTrain, DT_FuncTrackTrain ) END_SEND_TABLE() //----------------------------------------------------------------------------- // Constructor //----------------------------------------------------------------------------- CFuncTrackTrain::CFuncTrackTrain() { #ifdef _DEBUG m_controlMins.Init(); m_controlMaxs.Init(); #endif // These defaults match old func_tracktrains. Changing these defaults would // require a vmf_tweak of older content to keep it from breaking. m_eOrientationType = TrainOrientation_AtPathTracks; m_eVelocityType = TrainVelocity_Instantaneous; m_lastBlockPos.Init(); m_lastBlockTick = gpGlobals->tickcount; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CFuncTrackTrain::DrawDebugTextOverlays( void ) { int nOffset = BaseClass::DrawDebugTextOverlays(); if (m_debugOverlays & OVERLAY_TEXT_BIT) { char tempstr[512]; Q_snprintf( tempstr,sizeof(tempstr), "angles: %g %g %g", (double)GetLocalAngles()[PITCH], (double)GetLocalAngles()[YAW], (double)GetLocalAngles()[ROLL] ); EntityText( nOffset, tempstr, 0 ); nOffset++; float flCurSpeed = GetLocalVelocity().Length(); Q_snprintf( tempstr,sizeof(tempstr), "current speed (goal): %g (%g)", (double)flCurSpeed, (double)m_flSpeed ); EntityText( nOffset, tempstr, 0 ); nOffset++; Q_snprintf( tempstr,sizeof(tempstr), "max speed: %g", (double)m_maxSpeed ); EntityText( nOffset, tempstr, 0 ); nOffset++; } return nOffset; } void CFuncTrackTrain::DrawDebugGeometryOverlays() { BaseClass::DrawDebugGeometryOverlays(); if (m_debugOverlays & OVERLAY_BBOX_BIT) { NDebugOverlay::Box( GetAbsOrigin(), -Vector(4,4,4),Vector(4,4,4), 255, 0, 255, 0, 0); Vector out; VectorTransform( Vector(m_length,0,0), EntityToWorldTransform(), out ); NDebugOverlay::Box( out, -Vector(4,4,4),Vector(4,4,4), 255, 0, 255, 0, 0); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CFuncTrackTrain::KeyValue( const char *szKeyName, const char *szValue ) { if (FStrEq(szKeyName, "volume")) { m_flVolume = (float) (atoi(szValue)); m_flVolume *= 0.1f; } else { return BaseClass::KeyValue( szKeyName, szValue ); } return true; } //----------------------------------------------------------------------------- // Purpose: Input handler that stops the train. //----------------------------------------------------------------------------- void CFuncTrackTrain::InputStop( inputdata_t &inputdata ) { Stop(); } //------------------------------------------------------------------------------ // Purpose: Input handler that starts the train moving. //------------------------------------------------------------------------------ void CFuncTrackTrain::InputResume( inputdata_t &inputdata ) { m_flSpeed = m_oldSpeed; Start(); } //------------------------------------------------------------------------------ // Purpose: Input handler that reverses the trains current direction of motion. //------------------------------------------------------------------------------ void CFuncTrackTrain::InputReverse( inputdata_t &inputdata ) { SetDirForward( !IsDirForward() ); SetSpeed( m_flSpeed ); } //----------------------------------------------------------------------------- // Purpose: Returns whether we are travelling forward along our path. //----------------------------------------------------------------------------- bool CFuncTrackTrain::IsDirForward() { return ( m_dir == 1 ); } //----------------------------------------------------------------------------- // Purpose: Sets whether we go forward or backward along our path. //----------------------------------------------------------------------------- void CFuncTrackTrain::SetDirForward( bool bForward ) { if ( bForward && ( m_dir != 1 ) ) { // Reverse direction. if ( m_ppath && m_ppath->GetPrevious() ) { m_ppath = m_ppath->GetPrevious(); } m_dir = 1; } else if ( !bForward && ( m_dir != -1 ) ) { // Reverse direction. if ( m_ppath && m_ppath->GetNext() ) { m_ppath = m_ppath->GetNext(); } m_dir = -1; } } //------------------------------------------------------------------------------ // Purpose: Input handler that starts the train moving. //------------------------------------------------------------------------------ void CFuncTrackTrain::InputStartForward( inputdata_t &inputdata ) { SetDirForward( true ); SetSpeed( m_maxSpeed ); } //------------------------------------------------------------------------------ // Purpose: Input handler that starts the train moving. //------------------------------------------------------------------------------ void CFuncTrackTrain::InputStartBackward( inputdata_t &inputdata ) { SetDirForward( false ); SetSpeed( m_maxSpeed ); } //------------------------------------------------------------------------------ // Purpose: Starts the train moving. //------------------------------------------------------------------------------ void CFuncTrackTrain::Start( void ) { m_OnStart.FireOutput(this,this); Next(); } //----------------------------------------------------------------------------- // Purpose: Toggles the train between moving and not moving. //----------------------------------------------------------------------------- void CFuncTrackTrain::InputToggle( inputdata_t &inputdata ) { if ( m_flSpeed == 0 ) { SetSpeed( m_maxSpeed ); } else { SetSpeed( 0 ); } } //----------------------------------------------------------------------------- // Purpose: Handles player use so players can control the speed of the train. //----------------------------------------------------------------------------- void CFuncTrackTrain::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { // player +USE if ( useType == USE_SET ) { float delta = value; delta = ((int)(m_flSpeed * 4) / (int)m_maxSpeed)*0.25 + 0.25 * delta; if ( delta > 1 ) delta = 1; else if ( delta < -0.25 ) delta = -0.25; if ( m_spawnflags & SF_TRACKTRAIN_FORWARDONLY ) { if ( delta < 0 ) delta = 0; } SetDirForward( delta >= 0 ); delta = fabs(delta); SetSpeed( m_maxSpeed * delta ); } } //----------------------------------------------------------------------------- // Purpose: Input handler that sets the speed of the train. // Input : Float speed from 0 to max speed, in units per second. //----------------------------------------------------------------------------- void CFuncTrackTrain::InputSetSpeedReal( inputdata_t &inputdata ) { SetSpeed( clamp( inputdata.value.Float(), 0, m_maxSpeed ) ); } //----------------------------------------------------------------------------- // Purpose: Input handler that sets the speed of the train. // Input : Float speed scale from 0 to 1. //----------------------------------------------------------------------------- void CFuncTrackTrain::InputSetSpeed( inputdata_t &inputdata ) { float flScale = clamp( inputdata.value.Float(), 0, 1 ); SetSpeed( m_maxSpeed * flScale ); } //----------------------------------------------------------------------------- // Purpose: Input handler that sets the max speed of the train. // Input : Float speed //----------------------------------------------------------------------------- void CFuncTrackTrain::InputSetMaxSpeed( inputdata_t &inputdata ) { m_maxSpeed = inputdata.value.Float(); } //----------------------------------------------------------------------------- // Purpose: Input handler that sets the speed of the train and the direction // based on the sign of the speed. // Input : Float speed scale from -1 to 1. Negatives values indicate a reversed // direction. //----------------------------------------------------------------------------- void CFuncTrackTrain::InputSetSpeedDir( inputdata_t &inputdata ) { float newSpeed = inputdata.value.Float(); SetDirForward( newSpeed >= 0 ); newSpeed = fabs(newSpeed); float flScale = clamp( newSpeed, 0, 1 ); SetSpeed( m_maxSpeed * flScale ); } //----------------------------------------------------------------------------- // Purpose: Input handler that sets the speed of the train and the direction // based on the sign of the speed, and accels/decels to that speed // Input : Float speed scale from -1 to 1. Negatives values indicate a reversed // direction. //----------------------------------------------------------------------------- void CFuncTrackTrain::InputSetSpeedDirAccel( inputdata_t &inputdata ) { float newSpeed = inputdata.value.Float(); SetDirForward( newSpeed >= 0 ); newSpeed = fabs(newSpeed); float flScale = clamp( newSpeed, 0, 1 ); SetSpeed( m_maxSpeed * flScale, true ); } //----------------------------------------------------------------------------- // Purpose: Input handler that sets a target path node to move to. // Input : String name of the destination node //----------------------------------------------------------------------------- void CFuncTrackTrain::InputMoveToPathNode( inputdata_t &inputdata ) { m_strPathTarget = MAKE_STRING( inputdata.value.String() ); CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, inputdata.value.StringID() ); CPathTrack *pTrack, *pNext; pTrack = m_ppath; const int MAX_SEARCH_LENGTH = 1000; int searchesLeft = MAX_SEARCH_LENGTH; if (pTrack && pEntity) { float flDesiredSpeed = pTrack->m_flSpeed; if( pTrack->m_flSpeed == 0 ) { flDesiredSpeed = m_maxSpeed; } // if our current path is what we want then we can short circut. Still move forward - we will stop when we pass the track. if (pEntity == pTrack) { if (IsDirForward()) { if (pTrack->GetNext()) { SetDirForward( false ); SetSpeed( flDesiredSpeed ); return; } } else { if (pTrack->GetPrevious()) { SetDirForward( true ); SetSpeed( flDesiredSpeed ); return; } } Stop(); return; } do // check forward first { searchesLeft--; pNext = pTrack->GetNext(); if ( pNext ) pTrack = pNext; } while ( pNext && pEntity != pNext && searchesLeft); // quit if the next node is invalid - or our target if( pNext == pEntity ) { SetDirForward( true ); SetSpeed( flDesiredSpeed ); return; } searchesLeft = MAX_SEARCH_LENGTH; pTrack = m_ppath; // now reverse do { searchesLeft--; pNext = pTrack->GetPrevious(); if ( pNext ) pTrack = pNext; } while ( pNext && pEntity != pNext && searchesLeft); // quit if the next node is invalid - or our target if( pNext == pEntity ) { SetDirForward( false ); SetSpeed( flDesiredSpeed ); return; } } } //----------------------------------------------------------------------------- // Purpose: Input handler that teleports the train to the given node // Input : String name of the destination node //----------------------------------------------------------------------------- void CFuncTrackTrain::InputTeleportToPathNode( inputdata_t &inputdata ) { m_strPathTarget = MAKE_STRING( inputdata.value.String() ); CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, inputdata.value.StringID() ); if (pEntity) { m_ppath = (CPathTrack *)pEntity; ArriveAtNode( m_ppath ); TeleportToPathTrack( m_ppath ); } } //----------------------------------------------------------------------------- // Purpose: Lock the orientation of the train //----------------------------------------------------------------------------- void CFuncTrackTrain::InputLockOrientation( inputdata_t &inputdata ) { AddSpawnFlags( SF_TRACKTRAIN_FIXED_ORIENTATION ); SetLocalAngularVelocity( vec3_angle ); } //----------------------------------------------------------------------------- // Purpose: Lock the orientation of the train //----------------------------------------------------------------------------- void CFuncTrackTrain::InputUnlockOrientation( inputdata_t &inputdata ) { RemoveSpawnFlags( SF_TRACKTRAIN_FIXED_ORIENTATION ); } //----------------------------------------------------------------------------- // Purpose: Sets the speed of the train to the given value in units per second. //----------------------------------------------------------------------------- void CFuncTrackTrain::SetSpeed( float flSpeed, bool bAccel /*= false */ ) { m_bAccelToSpeed = bAccel; float flOldSpeed = m_flSpeed; if ( m_bAccelToSpeed ) { m_flDesiredSpeed = fabs( flSpeed ) * m_dir; m_flSpeedChangeTime = gpGlobals->curtime; if ( m_flSpeed == 0 && abs(m_flDesiredSpeed) > 0 ) { m_flSpeed = 0.1; // little push to get us going } Start(); return; } m_flSpeed = fabs( flSpeed ) * m_dir; if ( m_flSpeed != flOldSpeed) { // Changing speed. if ( m_flSpeed != 0 ) { if ( flOldSpeed == 0 ) { // Starting to move. Start(); } else { // Continuing to move. Next(); } } else { // Stopping. Stop(); } } DevMsg( 2, "TRAIN(%s), speed to %.2f\n", GetDebugName(), m_flSpeed ); } //----------------------------------------------------------------------------- // Purpose: Stops the train. //----------------------------------------------------------------------------- void CFuncTrackTrain::Stop( void ) { SetLocalVelocity( vec3_origin ); SetLocalAngularVelocity( vec3_angle ); m_oldSpeed = m_flSpeed; m_flSpeed = 0; SoundStop(); SetThink(NULL); } static CBaseEntity *FindPhysicsBlockerForHierarchy( CBaseEntity *pParentEntity ) { CUtlVector list; GetAllInHierarchy( pParentEntity, list ); CBaseEntity *pPhysicsBlocker = NULL; float maxForce = 0; for ( int i = 0; i < list.Count(); i++ ) { IPhysicsObject *pPhysics = list[i]->VPhysicsGetObject(); if ( pPhysics ) { IPhysicsFrictionSnapshot *pSnapshot = pPhysics->CreateFrictionSnapshot(); while ( pSnapshot->IsValid() ) { IPhysicsObject *pOther = pSnapshot->GetObject(1); CBaseEntity *pOtherEntity = static_cast(pOther->GetGameData()); if ( pOtherEntity->GetMoveType() == MOVETYPE_VPHYSICS ) { Vector normal; pSnapshot->GetSurfaceNormal(normal); float dot = DotProduct( pParentEntity->GetAbsVelocity(), pSnapshot->GetNormalForce() * normal ); if ( !pPhysicsBlocker || dot > maxForce ) { pPhysicsBlocker = pOtherEntity; maxForce = dot; } } pSnapshot->NextFrictionData(); } pPhysics->DestroyFrictionSnapshot( pSnapshot ); } } return pPhysicsBlocker; } //----------------------------------------------------------------------------- // Purpose: Called when we are blocked by another entity. // Input : pOther - //----------------------------------------------------------------------------- void CFuncTrackTrain::Blocked( CBaseEntity *pOther ) { // Blocker is on-ground on the train if ( ( pOther->GetFlags() & FL_ONGROUND ) && pOther->GetGroundEntity() == this ) { DevMsg( 1, "TRAIN(%s): Blocked by %s\n", GetDebugName(), pOther->GetClassname() ); float deltaSpeed = fabs(m_flSpeed); if ( deltaSpeed > 50 ) deltaSpeed = 50; Vector vecNewVelocity; pOther->GetVelocity( &vecNewVelocity ); if ( !vecNewVelocity.z ) { pOther->ApplyAbsVelocityImpulse( Vector(0,0,deltaSpeed) ); } return; } else { Vector vecNewVelocity; vecNewVelocity = pOther->GetAbsOrigin() - GetAbsOrigin(); VectorNormalize(vecNewVelocity); vecNewVelocity *= m_flBlockDamage; pOther->SetAbsVelocity( vecNewVelocity ); } if ( HasSpawnFlags(SF_TRACKTRAIN_UNBLOCKABLE_BY_PLAYER) ) { CBaseEntity *pPhysicsBlocker = FindPhysicsBlockerForHierarchy(this); if ( pPhysicsBlocker ) { // This code keeps track of how long this train has been blocked // The heuristic here is to keep instantaneous blocks from invoking the somewhat // heavy-handed solver (which will disable collisions until we're clear) in cases // where physics can solve it easily enough. int ticksBlocked = gpGlobals->tickcount - m_lastBlockTick; float dist = 0.0f; // wait at least 10 ticks and make sure the train isn't actually moving before really blocking const int MIN_BLOCKED_TICKS = 10; if ( ticksBlocked > MIN_BLOCKED_TICKS ) { dist = (GetAbsOrigin() - m_lastBlockPos).Length(); // must have moved at least 10% of normal velocity over the blocking interval, or we're being blocked float minLength = GetAbsVelocity().Length() * TICK_INTERVAL * MIN_BLOCKED_TICKS * 0.10f; if ( dist < minLength ) { // been stuck for more than one tick without moving much? // yes, disable collisions with the physics object most likely to be blocking us EntityPhysics_CreateSolver( this, pPhysicsBlocker, true, 4.0f ); } } // first time blocking or moved too far since last block, reset if ( dist > 1.0f || m_lastBlockTick < 0 ) { m_lastBlockPos = GetAbsOrigin(); m_lastBlockTick = gpGlobals->tickcount; } } // unblockable shouldn't damage the player in this case if ( pOther->IsPlayer() ) return; } DevWarning( 2, "TRAIN(%s): Blocked by %s (dmg:%.2f)\n", GetDebugName(), pOther->GetClassname(), m_flBlockDamage ); if ( m_flBlockDamage <= 0 ) return; // we can't hurt this thing, so we're not concerned with it pOther->TakeDamage( CTakeDamageInfo( this, this, m_flBlockDamage, DMG_CRUSH ) ); } extern void FixupAngles( QAngle &v ); #define TRAIN_MAXSPEED 1000 // approx max speed for sound pitch calculation //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CFuncTrackTrain::SoundStop( void ) { // if sound playing, stop it if ( m_bSoundPlaying ) { if ( m_iszSoundMove != NULL_STRING ) { StopSound( entindex(), CHAN_STATIC, STRING( m_iszSoundMove ) ); } if ( m_iszSoundStart != NULL_STRING ) { StopSound( entindex(), CHAN_STATIC, STRING( m_iszSoundStart ) ); } if ( m_iszSoundStop != NULL_STRING ) { CPASAttenuationFilter filter( this ); EmitSound_t ep; ep.m_nChannel = CHAN_ITEM; ep.m_pSoundName = STRING(m_iszSoundStop); ep.m_flVolume = m_flVolume; ep.m_SoundLevel = SNDLVL_NORM; EmitSound( filter, entindex(), ep ); } } m_bSoundPlaying = false; } //----------------------------------------------------------------------------- // Purpose: Update pitch based on speed, start sound if not playing. // NOTE: when train goes through transition, m_bSoundPlaying should become // false, which will cause the looped sound to restart. //----------------------------------------------------------------------------- void CFuncTrackTrain::SoundUpdate( void ) { if ( ( !m_iszSoundMove ) && ( !m_iszSoundStart ) && ( !m_iszSoundMovePing )) { return; } // In multiplayer, only update the sound once a second if ( g_pGameRules->IsMultiplayer() && m_bSoundPlaying ) { if ( m_flNextMPSoundTime > gpGlobals->curtime ) return; m_flNextMPSoundTime = gpGlobals->curtime + 1.0; } float flSpeedRatio = 0; if ( HasSpawnFlags( SF_TRACKTRAIN_USE_MAXSPEED_FOR_PITCH ) ) { flSpeedRatio = clamp( fabs( m_flSpeed ) / m_maxSpeed, 0, 1 ); } else { flSpeedRatio = clamp( fabs( m_flSpeed ) / TRAIN_MAXSPEED, 0, 1 ); } float flpitch = RemapVal( flSpeedRatio, 0, 1, m_nMoveSoundMinPitch, m_nMoveSoundMaxPitch ); CPASAttenuationFilter filter( this ); CPASAttenuationFilter filterReliable( this ); filterReliable.MakeReliable(); Vector vecWorldSpaceCenter = WorldSpaceCenter(); if (!m_bSoundPlaying) { if ( m_iszSoundStart != NULL_STRING ) { EmitSound_t ep; ep.m_nChannel = CHAN_ITEM; ep.m_pSoundName = STRING(m_iszSoundStart); ep.m_flVolume = m_flVolume; ep.m_SoundLevel = SNDLVL_NORM; ep.m_pOrigin = &vecWorldSpaceCenter; EmitSound( filter, entindex(), ep ); } if ( m_iszSoundMove != NULL_STRING ) { EmitSound_t ep; ep.m_nChannel = CHAN_STATIC; ep.m_pSoundName = STRING(m_iszSoundMove); ep.m_flVolume = m_flVolume; ep.m_SoundLevel = SNDLVL_NORM; ep.m_nPitch = (int)flpitch; ep.m_pOrigin = &vecWorldSpaceCenter; EmitSound( filterReliable, entindex(), ep ); } // We've just started moving. Delay the next move ping sound. m_flNextMoveSoundTime = gpGlobals->curtime + RemapVal( flSpeedRatio, 0, 1, m_flMoveSoundMaxTime, m_flMoveSoundMinTime ); m_bSoundPlaying = true; } else { if ( m_iszSoundMove != NULL_STRING ) { // update pitch EmitSound_t ep; ep.m_nChannel = CHAN_STATIC; ep.m_pSoundName = STRING(m_iszSoundMove); ep.m_flVolume = m_flVolume; ep.m_SoundLevel = SNDLVL_NORM; ep.m_nPitch = (int)flpitch; ep.m_nFlags = SND_CHANGE_PITCH; ep.m_pOrigin = &vecWorldSpaceCenter; // In multiplayer, don't make this reliable if ( g_pGameRules->IsMultiplayer() ) { EmitSound( filter, entindex(), ep ); } else { EmitSound( filterReliable, entindex(), ep ); } } if ( ( m_iszSoundMovePing != NULL_STRING ) && ( gpGlobals->curtime > m_flNextMoveSoundTime ) ) { EmitSound(STRING(m_iszSoundMovePing)); m_flNextMoveSoundTime = gpGlobals->curtime + RemapVal( flSpeedRatio, 0, 1, m_flMoveSoundMaxTime, m_flMoveSoundMinTime ); } } } //----------------------------------------------------------------------------- // Purpose: // Input : *pNode - //----------------------------------------------------------------------------- void CFuncTrackTrain::ArriveAtNode( CPathTrack *pNode ) { // BUGBUG: This is wrong. We need to fire all targets between the one we've passed and the one // we've switched to. FirePassInputs( pNode, pNode->GetNext(), true ); // // Disable train controls if this path track says to do so. // if ( pNode->HasSpawnFlags( SF_PATH_DISABLE_TRAIN ) ) { m_spawnflags |= SF_TRACKTRAIN_NOCONTROL; } // Do we have a node move target? if (m_strPathTarget != NULL_STRING) { CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, STRING(m_strPathTarget) ); if (pEntity && pNode == pEntity) { m_OnArrivedAtDestinationNode.FireOutput(pNode, this); m_strPathTarget = NULL_STRING; m_oldSpeed = m_flSpeed; m_flSpeed = 0; return; } } // // Don't override the train speed if it's under user control. // if ( m_spawnflags & SF_TRACKTRAIN_NOCONTROL ) { // // Don't copy speed from path track if it is 0 (uninitialized). // if ( pNode->m_flSpeed != 0 ) { SetSpeed( pNode->m_flSpeed ); DevMsg( 2, "TrackTrain %s arrived at %s, speed to %4.2f\n", GetDebugName(), pNode->GetDebugName(), pNode->m_flSpeed ); } } } //----------------------------------------------------------------------------- // Purpose: Controls how the train accelerates as it moves along the path. //----------------------------------------------------------------------------- TrainVelocityType_t CFuncTrackTrain::GetTrainVelocityType() { return m_eVelocityType; } //----------------------------------------------------------------------------- // Purpose: // Input : *pnext - //----------------------------------------------------------------------------- void CFuncTrackTrain::UpdateTrainVelocity( CPathTrack *pPrev, CPathTrack *pNext, const Vector &nextPos, float flInterval ) { switch ( GetTrainVelocityType() ) { case TrainVelocity_Instantaneous: { Vector velDesired = nextPos - GetLocalOrigin(); VectorNormalize( velDesired ); velDesired *= fabs( m_flSpeed ); if ( CBaseEntity::IsSimulatingOnAlternateTicks() && flInterval < .017 ) { // https://bugbait.valvesoftware.com/show_bug.cgi?id=70371 // Local band-aid: when a portal is on the train, the train simulates with the player some ticks but still only // every other tick, so the interval on game physics is one tick, but only run every other, so // you get a halting half speed effect. This isn't a great fix, as the movement of the panel is // not as smooth as w/o alternate ticks [11/15/2010 tom] velDesired *= 2.0f; } SetLocalVelocity( velDesired ); break; } case TrainVelocity_LinearBlend: case TrainVelocity_EaseInEaseOut: { if ( m_bAccelToSpeed ) { float flPrevSpeed = m_flSpeed; float flNextSpeed = m_flDesiredSpeed; if ( flPrevSpeed != flNextSpeed ) { float flSpeedChangeTime = ( abs(flNextSpeed) > abs(flPrevSpeed) ) ? m_flAccelSpeed : m_flDecelSpeed; m_flSpeed = UTIL_Approach( m_flDesiredSpeed, m_flSpeed, flSpeedChangeTime * gpGlobals->frametime ); } } else if ( pPrev && pNext ) { // Get the speed to blend from. float flPrevSpeed = m_flSpeed; if ( pPrev->m_flSpeed != 0 ) { flPrevSpeed = pPrev->m_flSpeed; } // Get the speed to blend to. float flNextSpeed = flPrevSpeed; if ( pNext->m_flSpeed != 0 ) { flNextSpeed = pNext->m_flSpeed; } // If they're different, do the blend. if ( flPrevSpeed != flNextSpeed ) { Vector vecSegment = pNext->GetLocalOrigin() - pPrev->GetLocalOrigin(); float flSegmentLen = vecSegment.Length(); if ( flSegmentLen ) { Vector vecCurOffset = GetLocalOrigin() - pPrev->GetLocalOrigin(); float p = vecCurOffset.Length() / flSegmentLen; if ( GetTrainVelocityType() == TrainVelocity_EaseInEaseOut ) { p = SimpleSplineRemapVal( p, 0.0f, 1.0f, 0.0f, 1.0f ); } m_flSpeed = m_dir * ( flPrevSpeed * ( 1 - p ) + flNextSpeed * p ); } } else { m_flSpeed = m_dir * flPrevSpeed; } } Vector velDesired = nextPos - GetLocalOrigin(); VectorNormalize( velDesired ); velDesired *= fabs( m_flSpeed ); SetLocalVelocity( velDesired ); break; } } } //----------------------------------------------------------------------------- // Purpose: Controls how the train blends angles as it moves along the path. //----------------------------------------------------------------------------- TrainOrientationType_t CFuncTrackTrain::GetTrainOrientationType() { return m_eOrientationType; } //----------------------------------------------------------------------------- // Purpose: // Input : pnext - //----------------------------------------------------------------------------- void CFuncTrackTrain::UpdateTrainOrientation( CPathTrack *pPrev, CPathTrack *pNext, const Vector &nextPos, float flInterval ) { // FIXME: old way of doing fixed orienation trains, remove! if ( HasSpawnFlags( SF_TRACKTRAIN_FIXED_ORIENTATION ) ) return; // Trains *can* work in local space, but only if all elements of the track share // the same move parent as the train. Assert( !pPrev || (pPrev->GetMoveParent() == GetMoveParent()) ); switch ( GetTrainOrientationType() ) { case TrainOrientation_Fixed: { // Fixed orientation. Do nothing. break; } case TrainOrientation_AtPathTracks: { UpdateOrientationAtPathTracks( pPrev, pNext, nextPos, flInterval ); break; } case TrainOrientation_EaseInEaseOut: case TrainOrientation_LinearBlend: { UpdateOrientationBlend( GetTrainOrientationType(), pPrev, pNext, nextPos, flInterval ); break; } } } //----------------------------------------------------------------------------- // Purpose: Adjusts our angles as we hit each path track. This is for support of // trains with wheels that round corners a la HL1 trains. // FIXME: move into path_track, have the angles come back from LookAhead //----------------------------------------------------------------------------- void CFuncTrackTrain::UpdateOrientationAtPathTracks( CPathTrack *pPrev, CPathTrack *pNext, const Vector &nextPos, float flInterval ) { if ( !m_ppath ) return; Vector nextFront = GetLocalOrigin(); CPathTrack *pNextNode = NULL; nextFront.z -= m_height; if ( m_length > 0 ) { m_ppath->LookAhead( nextFront, IsDirForward() ? m_length : -m_length, 0, &pNextNode ); } else { m_ppath->LookAhead( nextFront, IsDirForward() ? 100 : -100, 0, &pNextNode ); } nextFront.z += m_height; Vector vecFaceDir = nextFront - GetLocalOrigin(); if ( !IsDirForward() ) { vecFaceDir *= -1; } QAngle angles; VectorAngles( vecFaceDir, angles ); // !!! All of this crap has to be done to make the angles not wrap around, revisit this. FixupAngles( angles ); // Wrapped with this bool so we don't affect old trains if ( m_bManualSpeedChanges ) { if ( pNextNode && pNextNode->GetOrientationType() == TrackOrientation_FacePathAngles ) { angles = pNextNode->GetOrientation( IsDirForward() ); } } QAngle curAngles = GetLocalAngles(); FixupAngles( curAngles ); if ( !pPrev || (vecFaceDir.x == 0 && vecFaceDir.y == 0) ) angles = curAngles; DoUpdateOrientation( curAngles, angles, flInterval ); } //----------------------------------------------------------------------------- // Purpose: Blends our angles using one of two orientation blending types. // ASSUMES that eOrientationType is either LinearBlend or EaseInEaseOut. // FIXME: move into path_track, have the angles come back from LookAhead //----------------------------------------------------------------------------- void CFuncTrackTrain::UpdateOrientationBlend( TrainOrientationType_t eOrientationType, CPathTrack *pPrev, CPathTrack *pNext, const Vector &nextPos, float flInterval ) { // Get the angles to blend from. QAngle angPrev = pPrev->GetOrientation( IsDirForward() ); FixupAngles( angPrev ); // Get the angles to blend to. QAngle angNext; if ( pNext ) { angNext = pNext->GetOrientation( IsDirForward() ); FixupAngles( angNext ); } else { // At a dead end, just use the last path track's angles. angNext = angPrev; } if ( m_spawnflags & SF_TRACKTRAIN_NOPITCH ) { angNext[PITCH] = angPrev[PITCH]; } // Calculate our parametric distance along the path segment from 0 to 1. float p = 0; if ( pPrev && ( angPrev != angNext ) ) { Vector vecSegment = pNext->GetLocalOrigin() - pPrev->GetLocalOrigin(); float flSegmentLen = vecSegment.Length(); if ( flSegmentLen ) { Vector vecCurOffset = GetLocalOrigin() - pPrev->GetLocalOrigin(); p = vecCurOffset.Length() / flSegmentLen; } } if ( eOrientationType == TrainOrientation_EaseInEaseOut ) { p = SimpleSplineRemapVal( p, 0.0f, 1.0f, 0.0f, 1.0f ); } //Msg( "UpdateOrientationFacePathAngles: %s->%s, p=%f, ", pPrev->GetDebugName(), pNext->GetDebugName(), p ); Quaternion qtPrev; Quaternion qtNext; // hack to avoid gimble lock if( angPrev[PITCH] == 90 ) angPrev[PITCH] = 89; if( angPrev[PITCH] == -90 ) angPrev[PITCH] = -89; if( angNext[PITCH] == 90 ) angNext[PITCH] = 89; if( angNext[PITCH] == -90 ) angNext[PITCH] = 89; AngleQuaternion( angPrev, qtPrev ); AngleQuaternion( angNext, qtNext ); QAngle angNew = angNext; float flAngleDiff = QuaternionAngleDiff( qtPrev, qtNext ); if ( flAngleDiff ) { Quaternion qtNew; QuaternionSlerp( qtPrev, qtNext, p, qtNew ); QuaternionAngles( qtNew, angNew ); } if ( m_spawnflags & SF_TRACKTRAIN_NOPITCH ) { angNew[PITCH] = angPrev[PITCH]; } DoUpdateOrientation( GetLocalAngles(), angNew, flInterval ); } //----------------------------------------------------------------------------- // Purpose: Sets our angular velocity to approach the target angles over the given interval. //----------------------------------------------------------------------------- void CFuncTrackTrain::DoUpdateOrientation( const QAngle &curAngles, const QAngle &angles, float flInterval ) { float vy, vx, vz; if ( !(m_spawnflags & SF_TRACKTRAIN_NOPITCH) ) { vx = UTIL_AngleDistance( angles.x, curAngles.x ); } else { vx = 0; } vy = UTIL_AngleDistance( angles.y, curAngles.y ); if ( (m_spawnflags & SF_TRACKTRAIN_ALLOWROLL ) ) { vz = UTIL_AngleDistance( angles.z, curAngles.z ); } else { vz = 0; } // HACKHACK: Clamp really small angular deltas to avoid rotating movement on things // that are close enough if ( fabs(vx) < 0.1 ) { vx = 0; } if ( fabs(vy) < 0.1 ) { vy = 0; } if ( fabs(vz) < 0.1 ) { vz = 0; } if ( flInterval == 0 ) { // Avoid dividing by zero flInterval = 0.1; } QAngle vecAngVel( vx / flInterval, vy / flInterval, vz / flInterval ); if ( m_flBank != 0 ) { if ( vecAngVel.y < -5 ) { vecAngVel.z = UTIL_AngleDistance( UTIL_ApproachAngle( -m_flBank, curAngles.z, m_flBank*2 ), curAngles.z); } else if ( vecAngVel.y > 5 ) { vecAngVel.z = UTIL_AngleDistance( UTIL_ApproachAngle( m_flBank, curAngles.z, m_flBank*2 ), curAngles.z); } else { vecAngVel.z = UTIL_AngleDistance( UTIL_ApproachAngle( 0, curAngles.z, m_flBank*4 ), curAngles.z) * 4; } } SetLocalAngularVelocity( vecAngVel ); } //----------------------------------------------------------------------------- // Purpose: // Input : pTeleport - //----------------------------------------------------------------------------- void CFuncTrackTrain::TeleportToPathTrack( CPathTrack *pTeleport ) { QAngle angCur = GetLocalAngles(); Vector nextPos = pTeleport->GetLocalOrigin(); Vector look = nextPos; pTeleport->LookAhead( look, m_length, 0 ); QAngle nextAngles; if ( HasSpawnFlags( SF_TRACKTRAIN_FIXED_ORIENTATION ) || ( look == nextPos ) ) { nextAngles = GetLocalAngles(); } else { nextAngles = pTeleport->GetOrientation( IsDirForward() ); if ( HasSpawnFlags( SF_TRACKTRAIN_NOPITCH ) ) { nextAngles[PITCH] = angCur[PITCH]; } } Teleport( &pTeleport->GetLocalOrigin(), &nextAngles, NULL ); SetLocalAngularVelocity( vec3_angle ); } //----------------------------------------------------------------------------- // Purpose: Advances the train to the next path corner on the path. //----------------------------------------------------------------------------- void CFuncTrackTrain::Next( void ) { if ( !m_flSpeed ) { DevMsg( 2, "TRAIN(%s): Speed is 0\n", GetDebugName() ); SoundStop(); return; } if ( !m_ppath ) { DevMsg( 2, "TRAIN(%s): Lost path\n", GetDebugName() ); SoundStop(); m_flSpeed = 0; return; } SoundUpdate(); // // Based on our current position and speed, look ahead along our path and see // where we should be in 0.1 seconds. // Vector nextPos = GetLocalOrigin(); float flSpeed = m_flSpeed; nextPos.z -= m_height; CPathTrack *pNextNext = NULL; CPathTrack *pNext = m_ppath->LookAhead( nextPos, flSpeed * 0.1, 1, &pNextNext ); //Assert( pNext != NULL ); // If we're moving towards a dead end, but our desired speed goes in the opposite direction // this fixes us from stalling if ( m_bManualSpeedChanges && ( ( flSpeed < 0 ) != ( m_flDesiredSpeed < 0 ) ) ) { if ( !pNext ) pNext = m_ppath; } if (m_debugOverlays & OVERLAY_BBOX_BIT) { if ( pNext != NULL ) { NDebugOverlay::Line( GetAbsOrigin(), pNext->GetAbsOrigin(), 255, 0, 0, true, 0.1 ); NDebugOverlay::Line( pNext->GetAbsOrigin(), pNext->GetAbsOrigin() + Vector( 0,0,32), 255, 0, 0, true, 0.1 ); NDebugOverlay::Box( pNext->GetAbsOrigin(), Vector( -8, -8, -8 ), Vector( 8, 8, 8 ), 255, 0, 0, 0, 0.1 ); } if ( pNextNext != NULL ) { NDebugOverlay::Line( GetAbsOrigin(), pNextNext->GetAbsOrigin(), 0, 255, 0, true, 0.1 ); NDebugOverlay::Line( pNextNext->GetAbsOrigin(), pNextNext->GetAbsOrigin() + Vector( 0,0,32), 0, 255, 0, true, 0.1 ); NDebugOverlay::Box( pNextNext->GetAbsOrigin(), Vector( -8, -8, -8 ), Vector( 8, 8, 8 ), 0, 255, 0, 0, 0.1 ); } } nextPos.z += m_height; // Trains *can* work in local space, but only if all elements of the track share // the same move parent as the train. Assert( !pNext || (pNext->GetMoveParent() == GetMoveParent()) ); if ( pNext ) { UpdateTrainVelocity( pNext, pNextNext, nextPos, gpGlobals->frametime ); UpdateTrainOrientation( pNext, pNextNext, nextPos, gpGlobals->frametime ); if ( pNext != m_ppath ) { // // We have reached a new path track. Fire its OnPass output. // m_ppath = pNext; ArriveAtNode( pNext ); // // See if we should teleport to the next path track. // CPathTrack *pTeleport = pNext->GetNext(); if ( ( pTeleport != NULL ) && pTeleport->HasSpawnFlags( SF_PATH_TELEPORT ) ) { TeleportToPathTrack( pTeleport ); } } m_OnNext.FireOutput( pNext, this ); SetThink( &CFuncTrackTrain::Next ); SetMoveDoneTime( 0.1 ); SetNextThink( gpGlobals->curtime ); SetMoveDone( NULL ); } else { // // We've reached the end of the path, stop. // SoundStop(); SetLocalVelocity(nextPos - GetLocalOrigin()); SetLocalAngularVelocity( vec3_angle ); float distance = GetLocalVelocity().Length(); m_oldSpeed = m_flSpeed; m_flSpeed = 0; // Move to the dead end // Are we there yet? if ( distance > 0 ) { // no, how long to get there? float flTime = distance / fabs( m_oldSpeed ); SetLocalVelocity( GetLocalVelocity() * (fabs(m_oldSpeed) / distance) ); SetMoveDone( &CFuncTrackTrain::DeadEnd ); SetNextThink( TICK_NEVER_THINK ); SetMoveDoneTime( flTime ); } else { DeadEnd(); } } } Vector CFuncTrackTrain::ScriptGetFuturePosition( float flSeconds, float flMinSpeed ) { // // Based on our current position and speed, look ahead along our path and see // where we should be in flSeconds seconds. // Vector nextPos = GetLocalOrigin(); float flSpeed = flMinSpeed; nextPos.z -= m_height; CPathTrack *pNextNext = NULL; CPathTrack *pNext = m_ppath->LookAhead( nextPos, flSpeed * flSeconds, 1, &pNextNext ); // If we're moving towards a dead end, but our desired speed goes in the opposite direction // this fixes us from stalling if ( m_bManualSpeedChanges && ( ( flSpeed < 0 ) != ( m_flDesiredSpeed < 0 ) ) ) { if ( !pNext ) pNext = m_ppath; } // NDebugOverlay::Box( nextPos, Vector( -8, -8, -8 ), Vector( 8, 8, 8 ), 0, 255, 0, 0, 0.1 ); return nextPos; } void CFuncTrackTrain::FirePassInputs( CPathTrack *pStart, CPathTrack *pEnd, bool forward ) { CPathTrack *pCurrent = pStart; // swap if going backward if ( !forward ) { pCurrent = pEnd; pEnd = pStart; } variant_t emptyVariant; while ( pCurrent && pCurrent != pEnd ) { //Msg("Fired pass on %s\n", STRING(pCurrent->GetEntityName()) ); pCurrent->AcceptInput( "InPass", this, this, emptyVariant, 0 ); pCurrent = forward ? pCurrent->GetNext() : pCurrent->GetPrevious(); } } void CFuncTrackTrain::DeadEnd( void ) { // Fire the dead-end target if there is one CPathTrack *pTrack, *pNext; pTrack = m_ppath; DevMsg( 2, "TRAIN(%s): Dead end ", GetDebugName() ); // Find the dead end path node // HACKHACK -- This is bugly, but the train can actually stop moving at a different node depending on it's speed // so we have to traverse the list to it's end. if ( pTrack ) { if ( m_oldSpeed < 0 ) { do { pNext = pTrack->ValidPath( pTrack->GetPrevious(), true ); if ( pNext ) pTrack = pNext; } while ( pNext ); } else { do { pNext = pTrack->ValidPath( pTrack->GetNext(), true ); if ( pNext ) pTrack = pNext; } while ( pNext ); } } SetLocalVelocity( vec3_origin ); SetLocalAngularVelocity( vec3_angle ); if ( pTrack ) { m_ppath = pTrack; DevMsg( 2, "at %s\n", pTrack->GetDebugName() ); variant_t emptyVariant; pTrack->AcceptInput( "InPass", this, this, emptyVariant, 0 ); // also check to see if we were assigned to move here. if (m_strPathTarget != NULL_STRING ) { CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, STRING(m_strPathTarget) ); if (pEntity && pTrack == pEntity) { m_OnArrivedAtDestinationNode.FireOutput(pTrack, this); m_strPathTarget = NULL_STRING; } } } else { DevMsg( 2, "\n" ); } } void CFuncTrackTrain::SetControls( CBaseEntity *pControls ) { Vector offset = pControls->GetLocalOrigin(); m_controlMins = pControls->WorldAlignMins() + offset; m_controlMaxs = pControls->WorldAlignMaxs() + offset; } //----------------------------------------------------------------------------- // Purpose: Returns true if the entity's origin is within the controls region. //----------------------------------------------------------------------------- bool CFuncTrackTrain::OnControls( CBaseEntity *pTest ) { Vector offset = pTest->GetLocalOrigin() - GetLocalOrigin(); if ( m_spawnflags & SF_TRACKTRAIN_NOCONTROL ) return false; // Transform offset into local coordinates VMatrix tmp = SetupMatrixAngles( GetLocalAngles() ); // rotate into local space Vector local = tmp.VMul3x3Transpose( offset ); /* NDebugOverlay::Box( GetLocalOrigin(), m_controlMins, m_controlMaxs, 255, 0, 0, 100, 5.0 ); NDebugOverlay::Box( GetLocalOrigin() + local, Vector(-5,-5,-5), Vector(5,5,5), 0, 0, 255, 100, 5.0 ); */ if ( local.x >= m_controlMins.x && local.y >= m_controlMins.y && local.z >= m_controlMins.z && local.x <= m_controlMaxs.x && local.y <= m_controlMaxs.y && local.z <= m_controlMaxs.z ) return true; return false; } void CFuncTrackTrain::Find( void ) { m_ppath = (CPathTrack *)gEntList.FindEntityByName( NULL, m_target ); if ( !m_ppath ) return; if ( !FClassnameIs( m_ppath, "path_track" ) #ifndef PORTAL //env_portal_path_track is a child of path_track and would like to get found && !FClassnameIs( m_ppath, "env_portal_path_track" ) #endif //#ifndef PORTAL ) { Warning( "func_track_train must be on a path of path_track\n" ); Assert(0); m_ppath = NULL; return; } Vector nextPos = m_ppath->GetLocalOrigin(); Vector look = nextPos; m_ppath->LookAhead( look, m_length, 0 ); nextPos.z += m_height; look.z += m_height; QAngle nextAngles; if ( HasSpawnFlags( SF_TRACKTRAIN_FIXED_ORIENTATION ) ) { nextAngles = GetLocalAngles(); } else { VectorAngles( look - nextPos, nextAngles ); if ( HasSpawnFlags( SF_TRACKTRAIN_NOPITCH ) ) { nextAngles.x = 0; } } Teleport( &nextPos, &nextAngles, NULL ); ArriveAtNode( m_ppath ); if ( m_flSpeed != 0 ) { SetNextThink( gpGlobals->curtime + 0.1f ); SetThink( &CFuncTrackTrain::Next ); SoundUpdate(); } } void CFuncTrackTrain::NearestPath( void ) { CBaseEntity *pTrack = NULL; CBaseEntity *pNearest = NULL; float dist, closest; closest = 1024; for ( CEntitySphereQuery sphere( GetAbsOrigin(), 1024 ); ( pTrack = sphere.GetCurrentEntity() ) != NULL; sphere.NextEntity() ) { // filter out non-tracks if ( !(pTrack->GetFlags() & (FL_CLIENT|FL_NPC)) && FClassnameIs( pTrack, "path_track" ) ) { dist = (GetAbsOrigin() - pTrack->GetAbsOrigin()).Length(); if ( dist < closest ) { closest = dist; pNearest = pTrack; } } } if ( !pNearest ) { Msg( "Can't find a nearby track !!!\n" ); SetThink(NULL); return; } DevMsg( 2, "TRAIN: %s, Nearest track is %s\n", GetDebugName(), pNearest->GetDebugName() ); // If I'm closer to the next path_track on this path, then it's my real path pTrack = ((CPathTrack *)pNearest)->GetNext(); if ( pTrack ) { if ( (GetLocalOrigin() - pTrack->GetLocalOrigin()).Length() < (GetLocalOrigin() - pNearest->GetLocalOrigin()).Length() ) pNearest = pTrack; } m_ppath = (CPathTrack *)pNearest; if ( m_flSpeed != 0 ) { SetMoveDoneTime( 0.1 ); SetMoveDone( &CFuncTrackTrain::Next ); } } void CFuncTrackTrain::OnRestore( void ) { BaseClass::OnRestore(); if ( !m_ppath ) { NearestPath(); SetThink( NULL ); } } CFuncTrackTrain *CFuncTrackTrain::Instance( edict_t *pent ) { CBaseEntity *pEntity = CBaseEntity::Instance( pent ); if ( FClassnameIs( pEntity, "func_tracktrain" ) ) return (CFuncTrackTrain *)pEntity; return NULL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CFuncTrackTrain::Spawn( void ) { if ( m_maxSpeed == 0 ) { if ( m_flSpeed == 0 ) { m_maxSpeed = 100; } else { m_maxSpeed = m_flSpeed; } } if ( m_nMoveSoundMinPitch == 0 ) { m_nMoveSoundMinPitch = 60; } if ( m_nMoveSoundMaxPitch == 0 ) { m_nMoveSoundMaxPitch = 200; } SetLocalVelocity(vec3_origin); SetLocalAngularVelocity( vec3_angle ); m_dir = 1; if ( !m_target ) { Msg("FuncTrackTrain '%s' has no target.\n", GetDebugName()); } SetModel( STRING( GetModelName() ) ); SetMoveType( MOVETYPE_PUSH ); SetSolid( HasSpawnFlags( SF_TRACKTRAIN_HL1TRAIN ) ? SOLID_BSP : SOLID_VPHYSICS ); //SetSolid( SOLID_VPHYSICS ); if ( HasSpawnFlags( SF_TRACKTRAIN_UNBLOCKABLE_BY_PLAYER ) ) { AddFlag( FL_UNBLOCKABLE_BY_PLAYER ); } if ( m_spawnflags & SF_TRACKTRAIN_PASSABLE ) { AddSolidFlags( FSOLID_NOT_SOLID ); } m_controlMins = CollisionProp()->OBBMins(); m_controlMaxs = CollisionProp()->OBBMaxs(); m_controlMaxs.z += 72; // start trains on the next frame, to make sure their targets have had // a chance to spawn/activate SetThink( &CFuncTrackTrain::Find ); SetNextThink( gpGlobals->curtime ); Precache(); CreateVPhysics(); } bool CFuncTrackTrain::CreateVPhysics( void ) { VPhysicsInitShadow( false, false ); return true; } //----------------------------------------------------------------------------- // Purpose: Precaches the train sounds. //----------------------------------------------------------------------------- void CFuncTrackTrain::Precache( void ) { if (m_flVolume == 0.0) { m_flVolume = 1.0; } if ( m_iszSoundMove != NULL_STRING ) { PrecacheScriptSound( STRING( m_iszSoundMove ) ); } if ( m_iszSoundMovePing != NULL_STRING ) { PrecacheScriptSound( STRING( m_iszSoundMovePing ) ); } if ( m_iszSoundStart != NULL_STRING ) { PrecacheScriptSound( STRING( m_iszSoundStart ) ); } if ( m_iszSoundStop != NULL_STRING ) { PrecacheScriptSound( STRING( m_iszSoundStop ) ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CFuncTrackTrain::UpdateOnRemove() { SoundStop(); BaseClass::UpdateOnRemove(); } void CFuncTrackTrain::MoveDone() { m_lastBlockPos.Init(); m_lastBlockTick = -1; BaseClass::MoveDone(); } //----------------------------------------------------------------------------- // Purpose: Defines the volume of space that the player must stand in to // control the train //----------------------------------------------------------------------------- class CFuncTrainControls : public CBaseEntity { DECLARE_CLASS( CFuncTrainControls, CBaseEntity ); public: void Spawn( void ); void Find( void ); DECLARE_DATADESC(); }; BEGIN_DATADESC( CFuncTrainControls ) // Function Pointers DEFINE_FUNCTION( Find ), END_DATADESC() LINK_ENTITY_TO_CLASS( func_traincontrols, CFuncTrainControls ); void CFuncTrainControls::Find( void ) { CBaseEntity *pTarget = NULL; do { pTarget = gEntList.FindEntityByName( pTarget, m_target ); } while ( pTarget && !FClassnameIs(pTarget, "func_tracktrain") ); if ( !pTarget ) { Msg( "No train %s\n", STRING(m_target) ); return; } CFuncTrackTrain *ptrain = (CFuncTrackTrain*) pTarget; ptrain->SetControls( this ); SetThink( NULL ); } void CFuncTrainControls::Spawn( void ) { SetSolid( SOLID_NONE ); SetMoveType( MOVETYPE_NONE ); SetModel( STRING( GetModelName() ) ); AddEffects( EF_NODRAW ); Assert( GetParent() && "func_traincontrols needs parent to properly align to train" ); SetThink( &CFuncTrainControls::Find ); SetNextThink( gpGlobals->curtime ); } #define SF_TRACK_ACTIVATETRAIN 0x00000001 #define SF_TRACK_RELINK 0x00000002 #define SF_TRACK_ROTMOVE 0x00000004 #define SF_TRACK_STARTBOTTOM 0x00000008 #define SF_TRACK_DONT_MOVE 0x00000010 typedef enum { TRAIN_SAFE, TRAIN_BLOCKING, TRAIN_FOLLOWING } TRAIN_CODE; //----------------------------------------------------------------------------- // This entity is a rotating/moving platform that will carry a train to a new track. // It must be larger in X-Y planar area than the train, since it must contain the // train within these dimensions in order to operate when the train is near it. //----------------------------------------------------------------------------- class CFuncTrackChange : public CFuncPlatRot { DECLARE_CLASS( CFuncTrackChange, CFuncPlatRot ); public: void Spawn( void ); void Precache( void ); // virtual void Blocked( void ); virtual void GoUp( void ); virtual void GoDown( void ); void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); void Find( void ); TRAIN_CODE EvaluateTrain( CPathTrack *pcurrent ); void UpdateTrain( QAngle &dest ); virtual void HitBottom( void ); virtual void HitTop( void ); void Touch( CBaseEntity *pOther ); virtual void UpdateAutoTargets( int toggleState ); virtual bool IsTogglePlat( void ) { return true; } void DisableUse( void ) { m_use = 0; } void EnableUse( void ) { m_use = 1; } int UseEnabled( void ) { return m_use; } DECLARE_DATADESC(); CPathTrack *m_trackTop; CPathTrack *m_trackBottom; CFuncTrackTrain *m_train; string_t m_trackTopName; string_t m_trackBottomName; string_t m_trainName; TRAIN_CODE m_code; int m_targetState; int m_use; }; LINK_ENTITY_TO_CLASS( func_trackchange, CFuncTrackChange ); BEGIN_DATADESC( CFuncTrackChange ) DEFINE_GLOBAL_FIELD( m_trackTop, FIELD_CLASSPTR ), DEFINE_GLOBAL_FIELD( m_trackBottom, FIELD_CLASSPTR ), DEFINE_GLOBAL_FIELD( m_train, FIELD_CLASSPTR ), DEFINE_GLOBAL_KEYFIELD( m_trackTopName, FIELD_STRING, "toptrack" ), DEFINE_GLOBAL_KEYFIELD( m_trackBottomName, FIELD_STRING, "bottomtrack" ), DEFINE_GLOBAL_KEYFIELD( m_trainName, FIELD_STRING, "train" ), DEFINE_FIELD( m_code, FIELD_INTEGER ), DEFINE_FIELD( m_targetState, FIELD_INTEGER ), DEFINE_FIELD( m_use, FIELD_INTEGER ), // Function Pointers DEFINE_FUNCTION( Find ), END_DATADESC() void CFuncTrackChange::Spawn( void ) { Setup(); if ( FBitSet( m_spawnflags, SF_TRACK_DONT_MOVE ) ) m_vecPosition2.z = GetLocalOrigin().z; SetupRotation(); if ( FBitSet( m_spawnflags, SF_TRACK_STARTBOTTOM ) ) { UTIL_SetOrigin( this, m_vecPosition2); m_toggle_state = TS_AT_BOTTOM; SetLocalAngles( m_start ); m_targetState = TS_AT_TOP; } else { UTIL_SetOrigin( this, m_vecPosition1); m_toggle_state = TS_AT_TOP; SetLocalAngles( m_end ); m_targetState = TS_AT_BOTTOM; } EnableUse(); SetThink( &CFuncTrackChange::Find ); SetNextThink( gpGlobals->curtime + 2 ); Precache(); } void CFuncTrackChange::Precache( void ) { BaseClass::Precache(); PrecacheScriptSound( "FuncTrackChange.Blocking" ); } // UNDONE: Filter touches before re-evaluating the train. void CFuncTrackChange::Touch( CBaseEntity *pOther ) { } void CFuncTrackChange::Find( void ) { // Find track entities CBaseEntity *target; target = gEntList.FindEntityByName( NULL, m_trackTopName ); if ( target ) { m_trackTop = (CPathTrack*) target; target = gEntList.FindEntityByName( NULL, m_trackBottomName ); if ( target ) { m_trackBottom = (CPathTrack*) target; target = gEntList.FindEntityByName( NULL, m_trainName ); if ( target ) { m_train = (CFuncTrackTrain *)gEntList.FindEntityByName( NULL, m_trainName ); if ( !m_train ) { Warning( "Can't find train for track change! %s\n", STRING(m_trainName) ); Assert(0); return; } Vector center = WorldSpaceCenter(); m_trackBottom = m_trackBottom->Nearest( center ); m_trackTop = m_trackTop->Nearest( center ); UpdateAutoTargets( m_toggle_state ); SetThink( NULL ); return; } else { Warning( "Can't find train for track change! %s\n", STRING(m_trainName) ); Assert(0); target = gEntList.FindEntityByName( NULL, m_trainName ); } } else { Warning( "Can't find bottom track for track change! %s\n", STRING(m_trackBottomName) ); Assert(0); } } else { Warning( "Can't find top track for track change! %s\n", STRING(m_trackTopName) ); Assert(0); } } TRAIN_CODE CFuncTrackChange::EvaluateTrain( CPathTrack *pcurrent ) { // Go ahead and work, we don't have anything to switch, so just be an elevator if ( !pcurrent || !m_train ) return TRAIN_SAFE; if ( m_train->m_ppath == pcurrent || (pcurrent->m_pprevious && m_train->m_ppath == pcurrent->m_pprevious) || (pcurrent->m_pnext && m_train->m_ppath == pcurrent->m_pnext) ) { if ( m_train->m_flSpeed != 0 ) return TRAIN_BLOCKING; Vector dist = GetLocalOrigin() - m_train->GetLocalOrigin(); float length = dist.Length2D(); if ( length < m_train->m_length ) // Empirically determined close distance return TRAIN_FOLLOWING; else if ( length > (150 + m_train->m_length) ) return TRAIN_SAFE; return TRAIN_BLOCKING; } return TRAIN_SAFE; } void CFuncTrackChange::UpdateTrain( QAngle &dest ) { float time = GetMoveDoneTime(); m_train->SetAbsVelocity( GetAbsVelocity() ); m_train->SetLocalAngularVelocity( GetLocalAngularVelocity() ); m_train->SetMoveDoneTime( time ); // Attempt at getting the train to rotate properly around the origin of the trackchange if ( time <= 0 ) return; Vector offset = m_train->GetLocalOrigin() - GetLocalOrigin(); QAngle delta = dest - GetLocalAngles(); // Transform offset into local coordinates Vector forward, right, up; AngleVectorsTranspose( delta, &forward, &right, &up ); Vector local; local.x = DotProduct( offset, forward ); local.y = DotProduct( offset, right ); local.z = DotProduct( offset, up ); local = local - offset; m_train->SetAbsVelocity( GetAbsVelocity() + (local * (1.0/time)) ); } void CFuncTrackChange::GoDown( void ) { if ( m_code == TRAIN_BLOCKING ) return; // HitBottom may get called during CFuncPlat::GoDown(), so set up for that // before you call GoDown() UpdateAutoTargets( TS_GOING_DOWN ); // If ROTMOVE, move & rotate if ( FBitSet( m_spawnflags, SF_TRACK_DONT_MOVE ) ) { SetMoveDone( &CFuncTrackChange::CallHitBottom ); m_toggle_state = TS_GOING_DOWN; AngularMove( m_start, m_flSpeed ); } else { BaseClass::GoDown(); SetMoveDone( &CFuncTrackChange::CallHitBottom ); RotMove( m_start, GetMoveDoneTime() ); } // Otherwise, rotate first, move second // If the train is moving with the platform, update it if ( m_code == TRAIN_FOLLOWING ) { UpdateTrain( m_start ); m_train->m_ppath = NULL; } } // // Platform is at bottom, now starts moving up // void CFuncTrackChange::GoUp( void ) { if ( m_code == TRAIN_BLOCKING ) return; // HitTop may get called during CFuncPlat::GoUp(), so set up for that // before you call GoUp(); UpdateAutoTargets( TS_GOING_UP ); if ( FBitSet( m_spawnflags, SF_TRACK_DONT_MOVE ) ) { m_toggle_state = TS_GOING_UP; SetMoveDone( &CFuncTrackChange::CallHitTop ); AngularMove( m_end, m_flSpeed ); } else { // If ROTMOVE, move & rotate BaseClass::GoUp(); SetMoveDone( &CFuncTrackChange::CallHitTop ); RotMove( m_end, GetMoveDoneTime() ); } // Otherwise, move first, rotate second // If the train is moving with the platform, update it if ( m_code == TRAIN_FOLLOWING ) { UpdateTrain( m_end ); m_train->m_ppath = NULL; } } //----------------------------------------------------------------------------- // Purpose: Normal track change // Input : toggleState - //----------------------------------------------------------------------------- void CFuncTrackChange::UpdateAutoTargets( int toggleState ) { if ( !m_trackTop || !m_trackBottom ) return; if ( toggleState == TS_AT_TOP ) { m_trackTop->RemoveSpawnFlags( SF_PATH_DISABLED ); } else { m_trackTop->AddSpawnFlags( SF_PATH_DISABLED ); } if ( toggleState == TS_AT_BOTTOM ) { m_trackBottom->RemoveSpawnFlags( SF_PATH_DISABLED ); } else { m_trackBottom->AddSpawnFlags( SF_PATH_DISABLED ); } } void CFuncTrackChange::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { if ( m_toggle_state != TS_AT_TOP && m_toggle_state != TS_AT_BOTTOM ) return; // If train is in "safe" area, but not on the elevator, play alarm sound if ( m_toggle_state == TS_AT_TOP ) m_code = EvaluateTrain( m_trackTop ); else if ( m_toggle_state == TS_AT_BOTTOM ) m_code = EvaluateTrain( m_trackBottom ); else m_code = TRAIN_BLOCKING; if ( m_code == TRAIN_BLOCKING ) { // Play alarm and return EmitSound( "FuncTrackChange.Blocking" ); return; } // Otherwise, it's safe to move // If at top, go down // at bottom, go up DisableUse(); if (m_toggle_state == TS_AT_TOP) GoDown(); else GoUp(); } // // Platform has hit bottom. Stops and waits forever. // void CFuncTrackChange::HitBottom( void ) { BaseClass::HitBottom(); if ( m_code == TRAIN_FOLLOWING ) { // UpdateTrain(); m_train->SetTrack( m_trackBottom ); } SetMoveDone( NULL ); SetMoveDoneTime( -1 ); UpdateAutoTargets( m_toggle_state ); EnableUse(); } // // Platform has hit bottom. Stops and waits forever. // void CFuncTrackChange::HitTop( void ) { BaseClass::HitTop(); if ( m_code == TRAIN_FOLLOWING ) { // UpdateTrain(); m_train->SetTrack( m_trackTop ); } // Don't let the plat go back down SetMoveDone( NULL ); SetMoveDoneTime( -1 ); UpdateAutoTargets( m_toggle_state ); EnableUse(); } class CFuncTrackAuto : public CFuncTrackChange { DECLARE_CLASS( CFuncTrackAuto, CFuncTrackChange ); public: void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); virtual void UpdateAutoTargets( int toggleState ); void TriggerTrackChange( inputdata_t &inputdata ); DECLARE_DATADESC(); }; BEGIN_DATADESC( CFuncTrackAuto ) DEFINE_INPUTFUNC( FIELD_VOID, "Trigger", TriggerTrackChange ), END_DATADESC() LINK_ENTITY_TO_CLASS( func_trackautochange, CFuncTrackAuto ); // Auto track change void CFuncTrackAuto::UpdateAutoTargets( int toggleState ) { CPathTrack *pTarget, *pNextTarget; if ( !m_trackTop || !m_trackBottom ) return; if ( m_targetState == TS_AT_TOP ) { pTarget = m_trackTop->GetNext(); pNextTarget = m_trackBottom->GetNext(); } else { pTarget = m_trackBottom->GetNext(); pNextTarget = m_trackTop->GetNext(); } if ( pTarget ) { pTarget->RemoveSpawnFlags( SF_PATH_DISABLED ); if ( m_code == TRAIN_FOLLOWING && m_train && m_train->m_flSpeed == 0 ) { m_train->SetSpeed( pTarget->m_flSpeed ); m_train->Use( this, this, USE_SET, 0 ); } } if ( pNextTarget ) { pNextTarget->AddSpawnFlags( SF_PATH_DISABLED ); } } void CFuncTrackAuto::TriggerTrackChange ( inputdata_t &inputdata ) { CPathTrack *pTarget; if ( !UseEnabled() ) return; if ( m_toggle_state == TS_AT_TOP ) pTarget = m_trackTop; else if ( m_toggle_state == TS_AT_BOTTOM ) pTarget = m_trackBottom; else pTarget = NULL; if ( inputdata.pActivator && FClassnameIs( inputdata.pActivator, "func_tracktrain" ) ) { m_code = EvaluateTrain( pTarget ); // Safe to fire? if ( m_code == TRAIN_FOLLOWING && m_toggle_state != m_targetState ) { DisableUse(); if (m_toggle_state == TS_AT_TOP) GoDown(); else GoUp(); } } else { if ( pTarget ) pTarget = pTarget->GetNext(); if ( pTarget && m_train->m_ppath != pTarget && ShouldToggle( USE_TOGGLE, m_targetState ) ) { if ( m_targetState == TS_AT_TOP ) m_targetState = TS_AT_BOTTOM; else m_targetState = TS_AT_TOP; } UpdateAutoTargets( m_targetState ); } } void CFuncTrackAuto::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { CPathTrack *pTarget; if ( !UseEnabled() ) return; if ( m_toggle_state == TS_AT_TOP ) pTarget = m_trackTop; else if ( m_toggle_state == TS_AT_BOTTOM ) pTarget = m_trackBottom; else pTarget = NULL; if ( FClassnameIs( pActivator, "func_tracktrain" ) ) { m_code = EvaluateTrain( pTarget ); // Safe to fire? if ( m_code == TRAIN_FOLLOWING && m_toggle_state != m_targetState ) { DisableUse(); if (m_toggle_state == TS_AT_TOP) GoDown(); else GoUp(); } } else { if ( pTarget ) pTarget = pTarget->GetNext(); if ( pTarget && m_train->m_ppath != pTarget && ShouldToggle( useType, m_targetState ) ) { if ( m_targetState == TS_AT_TOP ) m_targetState = TS_AT_BOTTOM; else m_targetState = TS_AT_TOP; } UpdateAutoTargets( m_targetState ); } }