536 lines
14 KiB
C++
536 lines
14 KiB
C++
//===== 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<CFuncTrackTrain> 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<CFuncTrackTrain *>(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 );
|
|
}
|
|
}
|