//===== Copyright © 1996-2005, Valve Corporation, All rights reserved. ======// // // Purpose: // // $NoKeywords: $ //===========================================================================// #include "cbase.h" #include "trains.h" #include "entitylist.h" #include "soundenvelope.h" #include "engine/IEngineSound.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" extern int g_sModelIndexFireball; #define SPRITE_FIREBALL "sprites/zerogxplode.vmt" #define SPRITE_SMOKE "sprites/steam1.vmt" void UTIL_RemoveHierarchy( CBaseEntity *pDead ) { if ( !pDead ) return; if ( pDead->edict() ) { CBaseEntity *pChild = pDead->FirstMoveChild(); while ( pChild ) { CBaseEntity *pEntity = pChild; pChild = pChild->NextMovePeer(); UTIL_RemoveHierarchy( pEntity ); } } UTIL_Remove( pDead ); } class CFuncTankTrain : public CFuncTrackTrain { public: DECLARE_CLASS( CFuncTankTrain, CFuncTrackTrain ); void Spawn( void ); // Filter out damage messages that don't contain blast damage (impervious to other forms of attack) int OnTakeDamage( const CTakeDamageInfo &info ); void Event_Killed( const CTakeDamageInfo &info ); void Blocked( CBaseEntity *pOther ) { // FIxme, set speed to zero? } DECLARE_DATADESC(); private: COutputEvent m_OnDeath; }; LINK_ENTITY_TO_CLASS( func_tanktrain, CFuncTankTrain ); BEGIN_DATADESC( CFuncTankTrain ) // Outputs DEFINE_OUTPUT(m_OnDeath, "OnDeath"), END_DATADESC() void CFuncTankTrain::Spawn( void ) { m_takedamage = true; BaseClass::Spawn(); } // Filter out damage messages that don't contain blast damage (impervious to other forms of attack) int CFuncTankTrain::OnTakeDamage( const CTakeDamageInfo &info ) { if ( ! (info.GetDamageType() & DMG_BLAST) ) return 0; return BaseClass::OnTakeDamage( info ); } //----------------------------------------------------------------------------- // Purpose: Called when the train is killed. // Input : pInflictor - What killed us. // pAttacker - Who killed us. // flDamage - The damage that the killing blow inflicted. // bitsDamageType - Bitfield of damage types that were inflicted. //----------------------------------------------------------------------------- void CFuncTankTrain::Event_Killed( const CTakeDamageInfo &info ) { m_takedamage = DAMAGE_NO; m_lifeState = LIFE_DEAD; m_OnDeath.FireOutput( info.GetInflictor(), this ); } //----------------------------------------------------------------------------- // Purpose: Changes the target entity for a func_tank or tanktrain_ai //----------------------------------------------------------------------------- class CTankTargetChange : public CPointEntity { public: DECLARE_CLASS( CTankTargetChange, CPointEntity ); void Precache( void ); void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); DECLARE_DATADESC(); private: variant_t m_newTarget; string_t m_newTargetName; }; LINK_ENTITY_TO_CLASS( tanktrain_aitarget, CTankTargetChange ); BEGIN_DATADESC( CTankTargetChange ) // DEFINE_FIELD( m_newTarget, variant_t ), DEFINE_KEYFIELD( m_newTargetName, FIELD_STRING, "newtarget" ), END_DATADESC() void CTankTargetChange::Precache( void ) { BaseClass::Precache(); // This needs to be in Precache so save/load works m_newTarget.SetString( m_newTargetName ); } void CTankTargetChange::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, m_target, NULL, pActivator, pCaller ); // UNDONE: This should use more of the event system while ( pTarget ) { // Change the target over pTarget->AcceptInput( "TargetEntity", this, this, m_newTarget, 0 ); pTarget = gEntList.FindEntityByName( pTarget, m_target, NULL, pActivator, pCaller ); } } // UNDONE: Should be just a logical entity, but we act as another static sound channel for the train class CTankTrainAI : public CPointEntity { public: DECLARE_CLASS( CTankTrainAI, CPointEntity ); virtual ~CTankTrainAI( void ); void Precache( void ); void Spawn( void ); void Activate( void ); void Think( void ); int SoundEnginePitch( void ); void SoundEngineStart( void ); void SoundEngineStop( void ); void SoundShutdown( void ); CBaseEntity *FindTarget( string_t target, CBaseEntity *pActivator ); DECLARE_DATADESC(); // INPUTS void InputTargetEntity( inputdata_t &inputdata ); private: CHandle m_hTrain; EHANDLE m_hTargetEntity; int m_soundPlaying; CSoundPatch *m_soundTreads; CSoundPatch *m_soundEngine; string_t m_startSoundName; string_t m_engineSoundName; string_t m_movementSoundName; string_t m_targetEntityName; }; LINK_ENTITY_TO_CLASS( tanktrain_ai, CTankTrainAI ); BEGIN_DATADESC( CTankTrainAI ) DEFINE_FIELD( m_hTrain, FIELD_EHANDLE), DEFINE_FIELD( m_hTargetEntity, FIELD_EHANDLE), DEFINE_FIELD( m_soundPlaying, FIELD_INTEGER), DEFINE_SOUNDPATCH( m_soundTreads ), DEFINE_SOUNDPATCH( m_soundEngine ), DEFINE_KEYFIELD( m_startSoundName, FIELD_STRING, "startsound" ), DEFINE_KEYFIELD( m_engineSoundName, FIELD_STRING, "enginesound" ), DEFINE_KEYFIELD( m_movementSoundName, FIELD_STRING, "movementsound" ), DEFINE_FIELD( m_targetEntityName, FIELD_STRING), // Inputs DEFINE_INPUTFUNC( FIELD_STRING, "TargetEntity", InputTargetEntity ), END_DATADESC() //----------------------------------------------------------------------------- // Purpose: Input handler for setting the target entity by name. //----------------------------------------------------------------------------- void CTankTrainAI::InputTargetEntity( inputdata_t &inputdata ) { m_targetEntityName = inputdata.value.StringID(); m_hTargetEntity = FindTarget( m_targetEntityName, inputdata.pActivator ); SetNextThink( gpGlobals->curtime ); } //----------------------------------------------------------------------------- // Purpose: Finds the first entity in the entity list with the given name. // Input : target - String ID of the entity to find. // pActivator - The activating entity if this is called from an input // or Use handler, NULL otherwise. //----------------------------------------------------------------------------- CBaseEntity *CTankTrainAI::FindTarget( string_t target, CBaseEntity *pActivator ) { return gEntList.FindEntityGeneric( NULL, STRING( target ), this, pActivator ); } CTankTrainAI::~CTankTrainAI( void ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); if ( m_soundTreads ) { controller.SoundDestroy( m_soundTreads ); } if ( m_soundEngine ) { controller.SoundDestroy( m_soundEngine ); } } void CTankTrainAI::Precache( void ) { PrecacheScriptSound( STRING( m_startSoundName ) ); PrecacheScriptSound( STRING( m_engineSoundName ) ); PrecacheScriptSound( STRING( m_movementSoundName ) ); } int CTankTrainAI::SoundEnginePitch( void ) { CFuncTrackTrain *pTrain = m_hTrain; // we know this isn't NULL here if ( pTrain->GetMaxSpeed() ) { return 90 + (fabs(pTrain->GetCurrentSpeed()) * (20) / pTrain->GetMaxSpeed()); } return 100; } void CTankTrainAI::SoundEngineStart( void ) { CFuncTrackTrain *pTrain = m_hTrain; SoundEngineStop(); // play startup sound for train if ( m_startSoundName != NULL_STRING ) { CPASAttenuationFilter filter( pTrain ); EmitSound_t ep; ep.m_nChannel = CHAN_ITEM; ep.m_pSoundName = STRING(m_startSoundName); ep.m_flVolume = 1.0f; ep.m_SoundLevel = SNDLVL_NORM; EmitSound( filter, pTrain->entindex(), ep ); } // play the looping sounds using the envelope controller CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); if ( m_soundTreads ) { controller.Play( m_soundTreads, 1.0, 100 ); } if ( m_soundEngine ) { controller.Play( m_soundEngine, 0.5, 90 ); controller.CommandClear( m_soundEngine ); controller.CommandAdd( m_soundEngine, 0, SOUNDCTRL_CHANGE_PITCH, 1.5, random->RandomInt(130, 145) ); controller.CommandAdd( m_soundEngine, 1.5, SOUNDCTRL_CHANGE_PITCH, 2, random->RandomInt(105, 115) ); } m_soundPlaying = true; } void CTankTrainAI::SoundEngineStop( void ) { if ( !m_soundPlaying ) return; CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); if ( m_soundTreads ) { controller.SoundFadeOut( m_soundTreads, 0.25 ); } if ( m_soundEngine ) { controller.CommandClear( m_soundEngine ); controller.SoundChangePitch( m_soundEngine, 70, 3.0 ); } m_soundPlaying = false; } void CTankTrainAI::SoundShutdown( void ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); if ( m_soundTreads ) { controller.Shutdown( m_soundTreads ); } if ( m_soundEngine ) { controller.Shutdown( m_soundEngine ); } m_soundPlaying = false; } //----------------------------------------------------------------------------- // Purpose: Set up think and AI //----------------------------------------------------------------------------- void CTankTrainAI::Spawn( void ) { Precache(); m_soundPlaying = false; m_hTargetEntity = NULL; } void CTankTrainAI::Activate( void ) { BaseClass::Activate(); CBaseEntity *pTarget = NULL; CFuncTrackTrain *pTrain = NULL; if ( m_target != NULL_STRING ) { do { pTarget = gEntList.FindEntityByName( pTarget, m_target ); pTrain = dynamic_cast(pTarget); } while (!pTrain && pTarget); } m_hTrain = pTrain; if ( pTrain ) { SetNextThink( gpGlobals->curtime + 0.5f ); CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); if ( m_movementSoundName != NULL_STRING ) { CPASAttenuationFilter filter( this, ATTN_NORM * 0.5 ); m_soundTreads = controller.SoundCreate( filter, pTrain->entindex(), CHAN_STATIC, STRING(m_movementSoundName), ATTN_NORM*0.5 ); } if ( m_engineSoundName != NULL_STRING ) { CPASAttenuationFilter filter( this ); m_soundEngine = controller.SoundCreate( filter, pTrain->entindex(), CHAN_STATIC, STRING(m_engineSoundName), ATTN_NORM ); } } } //----------------------------------------------------------------------------- // Purpose: Dumb linear serach of the path // Input : *pStart - starting path node // &startPosition - starting position // &destination - position to move close to // Output : int move direction 1 = forward, -1 = reverse, 0 = stop //----------------------------------------------------------------------------- int PathFindDirection( CPathTrack *pStart, const Vector &startPosition, const Vector &destination ) { if ( !pStart ) return 0; // no path, don't move CPathTrack *pPath = pStart->m_pnext; CPathTrack *pNearest = pStart; float nearestDist = (pNearest->GetLocalOrigin() - destination).LengthSqr(); float length = 0; float nearestForward = 0, nearestReverse = 0; do { float dist = (pPath->GetLocalOrigin() - destination).LengthSqr(); // This is closer than our current estimate if ( dist < nearestDist ) { nearestDist = dist; pNearest = pPath; nearestForward = length; // current path length forward nearestReverse = 0; // count until we hit the start again } CPathTrack *pNext = pPath->m_pnext; if ( pNext ) { // UNDONE: Cache delta in path? float delta = (pNext->GetLocalOrigin() - pPath->GetLocalOrigin()).LengthSqr(); length += delta; // add to current reverse estimate nearestReverse += delta; pPath = pNext; } else { // not a looping path // traverse back to other end of the path int fail = 0; while ( pPath->m_pprevious ) { fail++; // HACKHACK: Don't infinite loop if ( fail > 256 ) break; pPath = pPath->m_pprevious; } // don't take the reverse path to old node nearestReverse = nearestForward + 1; // dont' take forward path to new node (if we find one) length = (float)COORD_EXTENT * (float)COORD_EXTENT; // HACKHACK: Max quad length } } while ( pPath != pStart ); // UNDONE: Fix this fudge factor // if you are already at the path, or <100 units away, don't move if ( pNearest == pStart || (pNearest->GetLocalOrigin() - startPosition).LengthSqr() < 100 ) return 0; if ( nearestForward <= nearestReverse ) return 1; return -1; } //----------------------------------------------------------------------------- // Purpose: Find a point on my path near to the target and move toward it //----------------------------------------------------------------------------- void CTankTrainAI::Think( void ) { CFuncTrackTrain *pTrain = m_hTrain; if ( !pTrain || pTrain->m_lifeState != LIFE_ALIVE ) { SoundShutdown(); if ( pTrain ) UTIL_RemoveHierarchy( pTrain ); UTIL_Remove( this ); return; } int desired = 0; CBaseEntity *pTarget = m_hTargetEntity; if ( pTarget ) { desired = PathFindDirection( pTrain->m_ppath, pTrain->GetLocalOrigin(), pTarget->GetLocalOrigin() ); } // If the train wants to stop, figure out throttle // otherwise, just throttle in the indicated direction and let the train logic // clip the speed if ( !desired ) { if ( pTrain->m_flSpeed > 0 ) { desired = -1; } else if ( pTrain->m_flSpeed < 0 ) { desired = 1; } } // UNDONE: Align the think time with arrival, and bump this up to a few seconds SetNextThink( gpGlobals->curtime + 0.5f ); if ( desired != 0 ) { int wasMoving = (pTrain->m_flSpeed == 0) ? false : true; // chaser wants train to move, send message pTrain->SetSpeed( desired ); int isMoving = (pTrain->m_flSpeed == 0) ? false : true; if ( !isMoving && wasMoving ) { SoundEngineStop(); } else if ( isMoving ) { if ( !wasMoving ) { SoundEngineStart(); } } } else { SoundEngineStop(); // UNDONE: Align the think time with arrival, and bump this up to a few seconds SetNextThink( gpGlobals->curtime + 1.0f ); } }