diff options
Diffstat (limited to 'game/server/tf/tf_passtime_ball.cpp')
| -rw-r--r-- | game/server/tf/tf_passtime_ball.cpp | 1437 |
1 files changed, 1437 insertions, 0 deletions
diff --git a/game/server/tf/tf_passtime_ball.cpp b/game/server/tf/tf_passtime_ball.cpp new file mode 100644 index 0000000..72ea39c --- /dev/null +++ b/game/server/tf/tf_passtime_ball.cpp @@ -0,0 +1,1437 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "tf_passtime_ball.h" +#include "tf_passtime_logic.h" +#include "passtime_ballcontroller.h" +#include "passtime_convars.h" +#include "passtime_game_events.h" +#include "func_passtime_no_ball_zone.h" +#include "tf_shareddefs.h" +#include "tf_player.h" +#include "vcollide_parse.h" +#include "SpriteTrail.h" +#include "soundenvelope.h" +#include "soundent.h" +#include "tf_gamerules.h" +#include "inetchannelinfo.h" +#include "tf_gamestats.h" +#include "tf_team.h" + +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +static const float s_flPickupDist = 1000.f; +static const float s_flBlockDist = 30.0f; +static const float s_flClearDist = 50.0f; +static const char *s_pHalloweenBallModel = "models/passtime/ball/passtime_ball_halloween.mdl"; + +//----------------------------------------------------------------------------- +static objectparams_t SBallVPhysicsObjectParams() +{ + objectparams_t params = g_PhysDefaultObjectParams; + params.mass = tf_passtime_ball_mass.GetFloat(); + params.dragCoefficient = tf_passtime_ball_drag_coefficient.GetFloat(); + params.damping = tf_passtime_ball_damping_scale.GetFloat(); + params.rotdamping = tf_passtime_ball_rotdamping_scale.GetFloat(); + params.inertia = tf_passtime_ball_inertia_scale.GetFloat(); + return params; +} + +//----------------------------------------------------------------------------- +// CBallPlayerToucher exists because we need the ball to touch both players and +// triggers. If the ball has FSOLID_TRIGGER, it will touch players but not +// triggers. And if it doesn't have that, it will touch triggers but not players. +// So this is a hack (there's probably a right way to do this) so the ball can +// just be solid and touch triggers, and this will touch players. +class CBallPlayerToucher : public CBaseEntity +{ +public: + DECLARE_CLASS( CBallPlayerToucher, CBaseEntity ); + CBallPlayerToucher() : m_pBall( 0 ) {} + + //----------------------------------------------------------------------------- + virtual void Spawn() OVERRIDE + { + // NOTE: this used to create its own vphysics sphere, but it turns out that + // the engine totally ignores it. + SetCollisionGroup( COLLISION_GROUP_PROJECTILE ); + SetModelIndex( m_pBall->GetModelIndex() ); + SetMoveType( MOVETYPE_NONE ); // DIFFERENT + m_takedamage = DAMAGE_NO; + SetNextThink( TICK_NEVER_THINK ); + m_iHealth = 0; + m_iMaxHealth = 1; + VPhysicsInitNormal( SOLID_NONE, 0, false ); + SetSolid( SOLID_VPHYSICS ); + SetSolidFlags( FSOLID_TRIGGER ); + SetMoveType( MOVETYPE_NONE ); // DIFFERENT + SetParent( m_pBall ); + SetLocalOrigin( Vector( 0,0,0 ) ); + SetLocalAngles( QAngle( 0,0,0 ) ); + SetTransmitState( FL_EDICT_DONTSEND ); + AddEffects( EF_NODRAW ); + SetTouch( &CBallPlayerToucher::OnTouch ); + } + + //----------------------------------------------------------------------------- + bool ShouldCollide( int iCollisionGroup, int iContentsMask ) const OVERRIDE + { + NOTE_UNUSED( iContentsMask ); + return iCollisionGroup == COLLISION_GROUP_PLAYER_MOVEMENT; + } + +private: + friend class CPasstimeBall; + CPasstimeBall *m_pBall; + + void OnTouch( CBaseEntity *pOther ) + { + m_pBall->OnTouch( pOther ); + } +}; + +LINK_ENTITY_TO_CLASS( _ballplayertoucher, CBallPlayerToucher ); + +//----------------------------------------------------------------------------- +IMPLEMENT_SERVERCLASS_ST( CPasstimeBall, DT_PasstimeBall ) + SendPropInt(SENDINFO(m_iCollisionCount)), + SendPropEHandle(SENDINFO(m_hHomingTarget)), + SendPropEHandle(SENDINFO(m_hCarrier)), + SendPropEHandle(SENDINFO(m_hPrevCarrier)), +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( passtime_ball, CPasstimeBall ); +PRECACHE_REGISTER( passtime_ball ); + +CTFPlayer *CPasstimeBall::GetCarrier() const { return m_hCarrier; } +CTFPlayer *CPasstimeBall::GetPrevCarrier() const { return m_hPrevCarrier; } + +//----------------------------------------------------------------------------- +CPasstimeBall::CPasstimeBall() +{ + m_bLeftOwner = false; + m_pHumLoop = 0; + m_pBeepLoop = 0; + m_pPlayerToucher = 0; + m_flLastTeamChangeTime = 0; + m_flBeginCarryTime = 0; + m_flIdleRespawnTime = 0; + m_bTrailActive = false; +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::Precache() +{ + PrecacheModel( "passtime/passtime_balltrail_red.vmt" ); + PrecacheModel( "passtime/passtime_balltrail_blu.vmt" ); + PrecacheModel( "passtime/passtime_balltrail_unassigned.vmt" ); + if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) ) + { + PrecacheModel( s_pHalloweenBallModel ); + } + else + { + PrecacheModel( tf_passtime_ball_model.GetString() ); + } + PrecacheScriptSound( "Passtime.BallSmack" ); + PrecacheScriptSound( "Passtime.BallGet" ); + PrecacheScriptSound( "Passtime.BallIdle" ); + PrecacheScriptSound( "Passtime.BallHoming" ); + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +CTFPlayer *CPasstimeBall::GetThrower() const +{ + return m_hThrower.Get(); +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::SetThrower( CTFPlayer *pPlayer ) +{ + m_hThrower = pPlayer; + if ( !pPlayer ) + { + ChangeTeam( TEAM_UNASSIGNED ); + } + else + { + ChangeTeam( pPlayer->GetTeamNumber() ); + } + +} + +//----------------------------------------------------------------------------- +unsigned int CPasstimeBall::PhysicsSolidMaskForEntity() const +{ + return MASK_PLAYERSOLID; // must include CONTENT_PLAYERCLIP +} + +//----------------------------------------------------------------------------- +int CPasstimeBall::GetCollisionCount() const { return m_iCollisionCount; } + +//----------------------------------------------------------------------------- +int CPasstimeBall::GetCarryDuration() const +{ + return ( (m_flBeginCarryTime > 0) && (m_flBeginCarryTime < gpGlobals->curtime) ) + ? (gpGlobals->curtime - m_flBeginCarryTime) + : 0; +} + + +//----------------------------------------------------------------------------- +static const char *GetTrailEffectForTeam( int iTeam ) +{ + switch ( iTeam ) + { + case TF_TEAM_RED: return "passtime/passtime_balltrail_red.vmt"; + case TF_TEAM_BLUE: return "passtime/passtime_balltrail_blu.vmt"; + default: return "passtime/passtime_balltrail_unassigned.vmt"; + }; +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::ChangeTeam( int iTeam ) +{ + // this isn't really the right place for this stats code, but its function + // is directly dependent on m_flLastTeamChangeTime so I wanted to keep it + // here to help avoid bugs creeping in. + // NOTE you can't rely on m_hCarrier being valid or correct here, the order + // of operations on calling ChangeTeam isn't stable between all the + // different places where it's called. + float flElapsedTimeOnThisTeam = gpGlobals->curtime - m_flLastTeamChangeTime; + if ( TFGameRules() && TFGameRules()->IsPasstimeMode() && g_pPasstimeLogic ) + { + gamerules_roundstate_t state = TFGameRules()->State_Get(); + if ( ((state == GR_STATE_RND_RUNNING) || (state == GR_STATE_STALEMATE) || (state == GR_STATE_TEAM_WIN)) && (flElapsedTimeOnThisTeam > 0) ) + { + int nElapsedTimeOnThisTeam = MAX( 0, Float2Int( flElapsedTimeOnThisTeam ) ); + if ( GetTeamNumber() == TEAM_UNASSIGNED ) + { + CTF_GameStats.m_passtimeStats.summary.nBallNeutralSec += nElapsedTimeOnThisTeam; + } + else + { + CTF_GameStats.m_passtimeStats.summary.nTotalCarrySec += nElapsedTimeOnThisTeam; + } + + CTFPlayer *pPlayer = GetThrower(); + if ( !pPlayer ) pPlayer = GetCarrier(); // this happens when the round ends or player dies or something + + if ( pPlayer ) + { + CTFTeam *pPlayerTeam = GetGlobalTFTeam( pPlayer->GetTeamNumber() ); + CTFTeam *pPlayerEnemyTeam = GetGlobalTFTeam( GetEnemyTeam( pPlayer->GetTeamNumber() ) ); + // NOTE: if the ball carrier switches teams and suicides, this will incorrectly + // attribute the time to the wrong team, but I don't care. + if ( pPlayerTeam->GetFlagCaptures() > pPlayerEnemyTeam->GetFlagCaptures() ) + { + CTF_GameStats.m_passtimeStats.summary.nTotalWinningTeamBallCarrySec += Float2Int( flElapsedTimeOnThisTeam ); + } + else if ( pPlayerTeam->GetFlagCaptures() < pPlayerEnemyTeam->GetFlagCaptures() ) + { + CTF_GameStats.m_passtimeStats.summary.nTotalLosingTeamBallCarrySec += Float2Int( flElapsedTimeOnThisTeam ); + } + } + } + } + + m_flLastTeamChangeTime = gpGlobals->curtime; + BaseClass::ChangeTeam( iTeam ); + + // teams: TEAM_UNASSIGNED, spectator, TF_TEAM_RED, TF_TEAM_BLUE + // skins: red, blu, unassigned + // NOTE: skins are in this order because we use the same model as the weapon viewmodel + // and m_bHasTeamSkins_Viewmodel expects them in this order + const int skinForTeam[] = { 2, 2, 0, 1 }; + iTeam = GetTeamNumber(); // paranoia; set by BaseClass::ChangeTeam + Assert( iTeam >= 0 && iTeam < 4 ); + if ( iTeam >= 0 && iTeam < 4 ) // paranoia + { + m_nSkin = skinForTeam[iTeam]; + } + + if ( m_bTrailActive ) + { + const char *pszTrailEffectName = GetTrailEffectForTeam( iTeam ); + m_pTrail->SetModel( pszTrailEffectName ); + } + + if ( iTeam == TEAM_UNASSIGNED ) + { + // NOTE: don't call SetThrower here, it'll be recursive. + m_hThrower = 0; + } +} + +//----------------------------------------------------------------------------- +bool CPasstimeBall::CreateModelCollider() +{ + solid_t tmpSolid; + PhysModelParseSolid( tmpSolid, this, GetModelIndex() ); + tmpSolid.params = SBallVPhysicsObjectParams(); + tmpSolid.params.pGameData = static_cast<void *>( this ); + + auto *pPhysObj = VPhysicsInitNormal( SOLID_VPHYSICS, 0, false, &tmpSolid ); + if ( !pPhysObj ) + { + return false; + } + + SetSolidFlags( FSOLID_NOT_STANDABLE ); + AddFlag( FL_GRENADE ); // required for airblast deflection to work + pPhysObj->Wake(); + + return true; +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::CreateSphereCollider() +{ + // NOTE: calling VPhysicsInitNormal(SOLID_BBOX) doesn't work right. + // Not calling SetSolid after also doesn't work right. + // In order for CreateSphereObject to work and not crash, you must do + // VPhysicsInitNormal( SOLID_NONE followed by SetSolid(whatever) + // Seems like VPHYSICS or BBOX do the same thing. + // Must have FSOLID_TRIGGER to touch players. Unfortunately, triggers can't trigger triggers. + + VPhysicsInitNormal( SOLID_NONE, 0, false ); + SetSolid( SOLID_VPHYSICS ); + SetSolidFlags( FSOLID_NOT_STANDABLE ); + AddFlag( FL_GRENADE ); // required for airblast deflection to work + + auto params = SBallVPhysicsObjectParams(); + params.pGameData = static_cast<void *>( this ); + const float flBallRadius = tf_passtime_ball_sphere_radius.GetFloat(); + const float flFourThirdsPi = 4.1888f; + params.volume = flFourThirdsPi * (flBallRadius*flBallRadius*flBallRadius); + + const int iPhysMat = physprops->GetSurfaceIndex("passtime_ball"); + IPhysicsObject *pPhysObj = physenv->CreateSphereObject( flBallRadius, iPhysMat, GetAbsOrigin(), GetAbsAngles(), ¶ms, false ); + VPhysicsSetObject( pPhysObj ); + SetMoveType( MOVETYPE_VPHYSICS ); + pPhysObj->Wake(); +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::Spawn() +{ + // not sure why this has to come first, but iirc it does. + SetCollisionGroup( COLLISION_GROUP_NONE ); + + // === CBaseProp::Spawn + const char *pszModelName = (char*) STRING( GetModelName() ); + if ( !pszModelName || !*pszModelName ) + { + if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) ) + { + pszModelName = s_pHalloweenBallModel; + } + else + { + pszModelName = tf_passtime_ball_model.GetString(); + } + } + PrecacheModel( pszModelName ); + Precache(); + SetModel( pszModelName ); + SetMoveType( MOVETYPE_PUSH ); + m_takedamage = DAMAGE_NO; + SetNextThink( TICK_NEVER_THINK ); + m_flAnimTime = gpGlobals->curtime; + m_flPlaybackRate = 0.0f; + SetCycle( 0 ); + + // === CBreakableProp::Spawn + m_flFadeScale = 1; + m_iHealth = 0; + m_takedamage = tf_passtime_ball_takedamage.GetBool() + ? DAMAGE_EVENTS_ONLY + : DAMAGE_NO; + m_iMaxHealth = 1; + + // === CPhysicsProp::Spawn + if( IsMarkedForDeletion() ) + { + return; + } + + m_pPlayerToucher = CreateEntityByName( "_ballplayertoucher" ); + ((CBallPlayerToucher*)m_pPlayerToucher)->m_pBall = this; + DispatchSpawn( m_pPlayerToucher ); + + if ( tf_passtime_ball_sphere_collision.GetBool() || !CreateModelCollider() ) + { + CreateSphereCollider(); + } + + // === My spawn + m_flLastTeamChangeTime = gpGlobals->curtime; + m_flBeginCarryTime = -1; + ResetTrail(); + ChangeTeam( TEAM_UNASSIGNED ); + + if ( TFGameRules()->IsPasstimeMode() ) + { + // TODO the ball used to be functional in non-wasabi maps, but I haven't maintained it + SetThink( &CPasstimeBall::DefaultThink ); + SetNextThink( gpGlobals->curtime ); + SetTransmitState( FL_EDICT_ALWAYS ); + m_playerSeek.SetIsEnabled( true ); + } + + m_flLastCollisionTime = gpGlobals->curtime; + m_flAirtimeDistance = 0; + m_eState = STATE_OUT_OF_PLAY; +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::SetIdleRespawnTime() +{ + auto *pTimer = TFGameRules()->GetActiveRoundTimer(); + if ( !pTimer ) return; + auto ts = pTimer->GetTimerState(); + auto grs = TFGameRules()->State_Get(); + m_flIdleRespawnTime = ((grs == GR_STATE_RND_RUNNING) && (ts == RT_STATE_NORMAL)) + ? (gpGlobals->curtime + tf_passtime_ball_reset_time.GetFloat()) + : 0; +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::DisableIdleRespawnTime() +{ + m_flIdleRespawnTime = 0; +} + +//----------------------------------------------------------------------------- +bool CPasstimeBall::ShouldCollide( int iCollisionGroup, int iContentsMask ) const +{ + // note: returning false for COLLISION_GROUP_PLAYER_MOVEMENT means the ball won't + // stop player movement. the only real visible effect when this function doesn't + // return false for COLLISION_GROUP_PLAYER_MOVEMENT is that the ball is unable + // to impart physics forces on itself when a player blocks it, since the player + // will set velocity to zero due to being "stuck" on the ball, even though the + // ball won't actually prevent the player from moving through it. + return (iCollisionGroup != COLLISION_GROUP_PLAYER_MOVEMENT); +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::ResetTrail() +{ + // ideally this would just drop all of the existing trail points instead of + // re-creating all the entities, but I couldn't find a clean way to do it in + // a reasonable amount of time. + HideTrail(); + + const char *pszTrailEffect = GetTrailEffectForTeam( GetTeamNumber() ); + Vector origin = GetAbsOrigin(); + float flStartRadius = tf_passtime_ball_sphere_radius.GetFloat() * 2; + float flEndRadius = tf_passtime_ball_sphere_radius.GetFloat() * 3; + m_pTrail = CSpriteTrail::SpriteTrailCreate( pszTrailEffect, origin, true ); + m_pTrail->SetAttachment( this, 0 ); + m_pTrail->SetTransmit( true ); // this actually controls whether the attachment parent receives it + m_pTrail->SetTransparency( kRenderTransAlpha, 255, 255, 255, 200, kRenderFxNone ); + m_pTrail->SetStartWidth( flStartRadius ); + m_pTrail->SetEndWidth( flEndRadius ); + m_pTrail->SetTextureResolution( 1 ); + m_pTrail->SetLifeTime( 3.0f ); + + m_bTrailActive = true; +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::HideTrail() +{ + // ideally this would just hide the existing trails instead of deleting + // them all, but I couldn't find a clean way to do it in a reasonable + // amount of time. + if ( !m_bTrailActive ) + { + return; + } + + // this is sometimes called from a physics callback (reset trail on collision) + // so use PhysCallbackRemove instead of UTIL_Remove + PhysCallbackRemove( m_pTrail->NetworkProp() ); + m_pTrail = nullptr; + m_bTrailActive = false; +} + +//----------------------------------------------------------------------------- +CPasstimeBall::~CPasstimeBall() +{ + // trail is automatically removed because it's a child + // m_pPlayerToucher is automatically removed because it's a child + + if ( m_pHumLoop ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pHumLoop ); + } + if ( m_pBeepLoop ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pBeepLoop ); + } +} + +//----------------------------------------------------------------------------- +// OnBecomeNotCarried: common boilerplate between SetStateFree/OutOfPlay +void CPasstimeBall::OnBecomeNotCarried() +{ + CTFPlayer *pCarrier = m_hCarrier; + + // + // Carrier management and events + // + if ( pCarrier && pCarrier->m_Shared.HasPasstimeBall() ) + { + pCarrier->m_Shared.SetHasPasstimeBall( false ); + pCarrier->m_Shared.RemoveCond( TF_COND_SPEED_BOOST, true ); + pCarrier->m_Shared.RemoveCond( TF_COND_PASSTIME_INTERCEPTION, true ); + pCarrier->TeamFortress_SetSpeed(); + PasstimeGameEvents::BallFree( pCarrier->entindex() ).Fire(); + } + + // + // Stats + // + if( m_flBeginCarryTime > 0 ) + { + int nClass = pCarrier->GetPlayerClass()->GetClassIndex(); + int nCarrySec = MAX( 0, Float2Int( gpGlobals->curtime - m_flBeginCarryTime ) ); + CTF_GameStats.m_passtimeStats.classes[ nClass].nTotalCarrySec += nCarrySec; + m_flBeginCarryTime = -1; + } + + // + // Reset various tracking and counters + // + m_iCollisionCount = 0; + m_flAirtimeDistance = 0; + m_flLastCollisionTime = gpGlobals->curtime; + m_bLeftOwner = false; + //m_playerSeek.SetIsEnabled( false ); // TODO: seek will re-enable itself + SetParent( 0 ); +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::SetStateFree() +{ + if ( BOutOfPlay() ) + { + // this is a hack to prevent the out-of-play time from counting in the stats + m_flLastTeamChangeTime = gpGlobals->curtime; + } + + // + // Change state + // + m_eState = STATE_FREE; + OnBecomeNotCarried(); + + // + // Make interactive + // + DisableIdleRespawnTime(); + RemoveEffects( EF_NODRAW ); + m_pPlayerToucher->RemoveSolidFlags( FSOLID_NOT_SOLID ); + m_pPlayerToucher->SetSolid( SOLID_VPHYSICS ); + m_takedamage = tf_passtime_ball_takedamage.GetBool() ? DAMAGE_EVENTS_ONLY : DAMAGE_NO; + SetMoveType( MOVETYPE_VPHYSICS ); + SetSolid( SOLID_VPHYSICS ); + SetSolidFlags( FSOLID_NOT_STANDABLE ); + SetThrower( m_hCarrier ); + TFGameRules()->SetObjectiveObserverTarget( this ); + VPhysicsGetObject()->EnableGravity( true ); + VPhysicsGetObject()->Wake(); + + // + // Trail management + // + if ( !m_bTrailActive ) + { + // create trails if there aren't any + ResetTrail(); + } + + // + // Sounds + // + if ( !m_pHumLoop ) + { + CReliableBroadcastRecipientFilter filter; + m_pHumLoop = CSoundEnvelopeController::GetController().SoundCreate( + filter, entindex(), "Passtime.BallIdle" ); + CSoundEnvelopeController::GetController().Play( m_pHumLoop, 1, PITCH_NORM ); + } + + // + // Bookeeping + // + if ( m_hCarrier ) + { + m_hPrevCarrier = m_hCarrier; + } + m_hCarrier = 0; +} + +//----------------------------------------------------------------------------- +bool CPasstimeBall::BOutOfPlay() const { return m_eState == STATE_OUT_OF_PLAY; } + +//----------------------------------------------------------------------------- +void CPasstimeBall::SetStateOutOfPlay() +{ + // This can be called redundantly during RespawnBall + if ( BOutOfPlay() ) + { + return; + } + + // this is a hack to make sure the carrier stats are captured because + // ChangeTeam updates some stats and may not be called at end of round. + ChangeTeam( TEAM_UNASSIGNED ); + + // + // Change state + // + m_eState = STATE_OUT_OF_PLAY; + OnBecomeNotCarried(); + + // + // Make noninteractive + // + DisableIdleRespawnTime(); + AddEffects( EF_NODRAW ); + m_pPlayerToucher->AddSolidFlags( FSOLID_NOT_SOLID ); + m_pPlayerToucher->SetSolid( SOLID_NONE ); + m_takedamage = DAMAGE_NO; + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_NONE ); + SetSolidFlags( FSOLID_NOT_SOLID ); + SetThrower( 0 ); + TFGameRules()->SetObjectiveObserverTarget( 0 ); + VPhysicsGetObject()->EnableGravity( false ); + + // + // Trail management + // + HideTrail(); + + // + // Sounds + // + if ( m_pHumLoop ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pHumLoop ); + m_pHumLoop = 0; + } + + if ( m_pBeepLoop ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pBeepLoop ); + m_pBeepLoop = 0; + } + + // + // Bookeeping + // + if ( m_hCarrier ) + { + m_hPrevCarrier = m_hCarrier; + } + m_hCarrier = 0; +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::SetStateCarried( CTFPlayer *pCarrier ) +{ + // this can be called when m_eState==STATE_CARRIED when the ball is being + // directly transferred between players. + m_eState = STATE_CARRIED; + + Assert( pCarrier ); + if ( !pCarrier ) + { + SetStateOutOfPlay(); + return; + } + + // + // Carrier management and events + // FIXME move all of the event handling for ball events into CTFPasstimeLogic + // + Assert( !pCarrier->m_Shared.HasPasstimeBall() ); + pCarrier->RemoveInvisibility(); + pCarrier->RemoveDisguise(); + pCarrier->EndClassSpecialSkill(); // abort demo charge + pCarrier->m_Shared.SetHasPasstimeBall( true ); + if ( pCarrier != m_hPrevCarrier ) + { + pCarrier->m_Shared.AddCond( TF_COND_SPEED_BOOST, tf_passtime_speedboost_on_get_ball_time.GetFloat() ); + + // Limit points by time so we can't just throw back and forth a ton for points. + // FIXME awarding points here and also in passtime_logic? + if ( gpGlobals->realtime - g_pPasstimeLogic->GetLastPassTime(pCarrier) > 6.0f ) // FIXME literal balance value + { + CTF_GameStats.Event_PlayerAwardBonusPoints(pCarrier, 0, 5); // FIXME literal balance value + g_pPasstimeLogic->SetLastPassTime(pCarrier); + } + } + pCarrier->TeamFortress_SetSpeed(); + + // + // Adjust things common to all states + // + DisableIdleRespawnTime(); + AddEffects( EF_NODRAW ); + m_iCollisionCount = 0; + m_flAirtimeDistance = 0; + m_flLastCollisionTime = gpGlobals->curtime; + m_bLeftOwner = false; + //m_playerSeek.SetIsEnabled( false ); // TODO: seek will re-enable itself + m_pPlayerToucher->AddSolidFlags( FSOLID_NOT_SOLID ); + m_pPlayerToucher->SetSolid( SOLID_NONE ); + m_takedamage = DAMAGE_NO; + SetMoveType( MOVETYPE_NONE ); + SetParent( pCarrier, pCarrier->LookupAttachment( "effect_hand_R" ) ); + SetSolid( SOLID_NONE ); + SetSolidFlags( FSOLID_NOT_SOLID ); + TFGameRules()->SetObjectiveObserverTarget( pCarrier ); + VPhysicsGetObject()->EnableGravity( false ); + + // + // Unique to this state + // + m_bTouchedSinceSpawn = true; + SetLocalOrigin( Vector( 0,0,0 ) ); // because SetParent(pCarrier) + + // + // Sounds + // + EmitSound( "Passtime.BallGet" ); + if ( m_pHumLoop ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pHumLoop ); + m_pHumLoop = 0; + } + + if ( m_pBeepLoop ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pBeepLoop ); + m_pBeepLoop = 0; + } + + // + // Stats + // + m_flBeginCarryTime = gpGlobals->curtime; + + // + // Bookeeping + // + if ( m_hCarrier ) + { + m_hPrevCarrier = m_hCarrier; + } + m_hCarrier = pCarrier; + ChangeTeam( pCarrier->GetTeamNumber() ); +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::MoveToSpawner( const Vector &pos ) +{ + MoveTo( pos, Vector( 0,0,0 ) ); + m_bTouchedSinceSpawn = false; + m_hPrevCarrier = 0; +} + +//----------------------------------------------------------------------------- +bool CPasstimeBall::IsDeflectable() +{ + return m_eState == STATE_FREE; +} + +//----------------------------------------------------------------------------- +int CPasstimeBall::UpdateTransmitState() +{ + if ( !TFGameRules()->IsPasstimeMode() ) + { + return BaseClass::UpdateTransmitState(); + } + return SetTransmitState(FL_EDICT_ALWAYS); +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::MoveTo( const Vector &pos, const Vector &vecVel ) +{ + // NOTE: using Teleport() causes some weird interpolation errors + // because it handles it specially as a "teleport list" etc + + SetAbsOrigin( pos ); + SetAbsVelocity( vecVel ); + SetAbsAngles( QAngle( 0, 0, 0 ) ); + + IPhysicsObject *pPhys = VPhysicsGetObject(); + + pPhys->SetPosition( pos, QAngle( 0, 0, 0 ), true ); + Vector fwd = vecVel.Normalized(); + AngularImpulse angular( fwd.x * 0, fwd.y * 0, fwd.z * 1 ); // TODO + pPhys->SetVelocity( &vecVel, &angular ); + + PhysicsTouchTriggers(); + + m_vecPrevOrigin = pos; // used for tracking pass distance + + CPasstimeBallController::BallSpawned( this ); +} + +//----------------------------------------------------------------------------- +bool CPasstimeBall::BShouldPanicRespawn() const +{ + if ( !TFGameRules() + || ( TFGameRules()->State_Get() != GR_STATE_RND_RUNNING ) + || ( m_eState != STATE_FREE ) ) + { + return false; + } + + if ( ( m_flIdleRespawnTime > 0 ) && ( m_flIdleRespawnTime < gpGlobals->curtime ) ) + { + return true; + } + + return ( enginetrace->GetPointContents( GetAbsOrigin() ) == CONTENTS_SOLID ); +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::DefaultThink() +{ + UpdateLagCompensationHistory(); + + if( IsMarkedForDeletion() || !g_pPasstimeLogic ) + { + return; + } + + SetNextThink( gpGlobals->curtime ); + + if ( BShouldPanicRespawn() ) + { + g_pPasstimeLogic->RespawnBall(); + return; + } + + // + // Eject the ball if the carrier isn't allowed to carry it + // + CTFPlayer *pCarrier = m_hCarrier; + if ( pCarrier ) + { + HudNotification_t ejectReason; + if ( !g_pPasstimeLogic->BCanPlayerPickUpBall( pCarrier, &ejectReason ) ) + { + if ( ejectReason && TFGameRules() ) + { + CSingleUserReliableRecipientFilter filter( pCarrier ); + TFGameRules()->SendHudNotification( filter, ejectReason ); + } + g_pPasstimeLogic->EjectBall( pCarrier, pCarrier ); + SetIdleRespawnTime(); // have to do this here because need to guarantee it happens for no ball zones + EmitSound( "Passtime.BallDropped"); + return; + } + } + + // + // Track airtime and apply controllers + // + if ( m_eState == STATE_FREE ) + { + { + Vector vecOrigin = GetAbsOrigin(); + m_flAirtimeDistance += vecOrigin.DistTo( m_vecPrevOrigin ); + m_vecPrevOrigin = vecOrigin; + } + + IPhysicsObject *pPhysObj = VPhysicsGetObject(); + Vector vecVel; + pPhysObj->GetVelocity( &vecVel, 0 ); + SetAbsVelocity( vecVel ); + // this is a hack to work around some issues where GetAbsVelocity was just + // returning some huge value. this seems to fix it, so something is probably fubar in physics :/ + // hopefully just related to using the sphere collider that nothing else uses. + + pPhysObj->Wake(); // NEVER SLEEP + + //m_playerSeek.SetIsEnabled( !m_bTouchedSinceSpawn ); + CPasstimeBallController::ApplyTo( this ); + } +} + +//----------------------------------------------------------------------------- +extern ConVar sv_maxunlag; +void CPasstimeBall::UpdateLagCompensationHistory() +{ + // adapted from CLagCompensationManager::FrameUpdatePostEntityThink + + Assert( m_lagCompensationHistory.Count() < 1000 ); // insanity check + m_flLagCompensationTeleportDistanceSqr = 64*64; + + // remove tail records that are too old + int tailIndex = m_lagCompensationHistory.Tail(); + int flDeadtime = gpGlobals->curtime - sv_maxunlag.GetFloat(); + while ( m_lagCompensationHistory.IsValidIndex( tailIndex ) ) + { + LagRecord &tail = m_lagCompensationHistory.Element( tailIndex ); + + // if tail is within limits, stop + if ( tail.flSimulationTime >= flDeadtime ) + break; + + // remove tail, get new tail + m_lagCompensationHistory.Remove( tailIndex ); + tailIndex = m_lagCompensationHistory.Tail(); + } + + // check if head has same simulation time + if ( m_lagCompensationHistory.Count() > 0 ) + { + LagRecord &head = m_lagCompensationHistory.Element( m_lagCompensationHistory.Head() ); + + // check if player changed simulation time since last time updated + if ( head.flSimulationTime >= GetSimulationTime() ) + return; // don't add new entry for same or older time + } + + // add new record to player track + LagRecord &record = m_lagCompensationHistory.Element( m_lagCompensationHistory.AddToHead() ); + record.flSimulationTime = GetSimulationTime(); + record.vecOrigin = GetAbsOrigin(); +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::StartLagCompensation( CBasePlayer *player, CUserCmd *cmd ) +{ + m_bLagCompensationNeedsRestore = false; // set to true if it actually backtracks + if ( m_lagCompensationHistory.Count() <= 0 ) + return; + + // adapted from CLagCompensationManager::StartLagCompensation + + int targettick = cmd->tick_count; + { + // correct is the amout of time we have to correct game time + float correct = 0.0f; + + INetChannelInfo *nci = engine->GetPlayerNetInfo( player->entindex() ); + + if ( nci ) + { + // add network latency + correct+= nci->GetLatency( FLOW_OUTGOING ); + } + + // calc number of view interpolation ticks - 1 + int lerpTicks = TIME_TO_TICKS( player->m_fLerpTime ); + + // add view interpolation latency see C_BaseEntity::GetInterpolationAmount() + correct += TICKS_TO_TIME( lerpTicks ); + + // check bouns [0,sv_maxunlag] + correct = clamp( correct, 0.0f, sv_maxunlag.GetFloat() ); + + // correct tick send by player + targettick = cmd->tick_count - lerpTicks; + + // calc difference between tick send by player and our latency based tick + float deltaTime = correct - TICKS_TO_TIME(gpGlobals->tickcount - targettick); + + if ( fabs( deltaTime ) > 0.2f ) + { + // difference between cmd time and latency is too big > 200ms, use time correction based on latency + // DevMsg("StartLagCompensation: delta too big (%.3f)\n", deltaTime ); + targettick = gpGlobals->tickcount - TIME_TO_TICKS( correct ); + } + } + + // copied from BacktrackPlayer + Vector org; + float flTargetTime = TICKS_TO_TIME( targettick ); + { + int curr = m_lagCompensationHistory.Head(); + LagRecord *prevRecord = 0; + LagRecord *record = 0; + Vector prevOrg = GetAbsOrigin(); + + // Walk context looking for any invalidating pEvent + while( m_lagCompensationHistory.IsValidIndex(curr) ) + { + // remember last record + prevRecord = record; + + // get next record + record = &m_lagCompensationHistory.Element( curr ); + + Vector delta = record->vecOrigin - prevOrg; + if ( delta.Length2DSqr() > m_flLagCompensationTeleportDistanceSqr ) + { + // lost track, too much difference + return; + } + + // did we find a context smaller than target time ? + if ( record->flSimulationTime <= flTargetTime ) + break; // hurra, stop + + prevOrg = record->vecOrigin; + + // go one step back + curr = m_lagCompensationHistory.Next( curr ); + } + + Assert( record ); + if ( !record ) + { + return; // that should never happen + } + + + float frac = 0.0f; + if ( prevRecord && + (record->flSimulationTime < flTargetTime) && + (record->flSimulationTime < prevRecord->flSimulationTime) ) + { + // we didn't find the exact time but have a valid previous record + // so interpolate between these two records; + + Assert( prevRecord->flSimulationTime > record->flSimulationTime ); + Assert( flTargetTime < prevRecord->flSimulationTime ); + + // calc fraction between both records + frac = ( flTargetTime - record->flSimulationTime ) / + ( prevRecord->flSimulationTime - record->flSimulationTime ); + + Assert( frac > 0 && frac < 1 ); // should never extrapolate + + org = Lerp( frac, record->vecOrigin, prevRecord->vecOrigin ); + } + else + { + // we found the exact record or no other record to interpolate with + // just copy these values since they are the best we have + org = record->vecOrigin; + } + } + + Vector orgdiff = GetAbsOrigin() - org; + m_lagCompensationRestore.flSimulationTime = GetSimulationTime(); + m_lagCompensationRestore.vecOrigin = GetAbsOrigin(); + SetAbsOrigin( org ); + SetSimulationTime( flTargetTime ); + m_bLagCompensationNeedsRestore = true; +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::FinishLagCompensation( CBasePlayer *player ) +{ + // adapted from CLagCompensationManager::BacktrackPlayer + + if ( !m_bLagCompensationNeedsRestore ) + { + return; + } + + SetAbsOrigin( m_lagCompensationRestore.vecOrigin ); // this is probably not correct? + SetSimulationTime( m_lagCompensationRestore.flSimulationTime ); +} + +//----------------------------------------------------------------------------- +bool CPasstimeBall::BIgnorePlayer( CTFPlayer *pPlayer ) +{ + // NOTE: it's possible to be !alive and !dead at the same time + if ( !pPlayer || !pPlayer->IsAlive() ) + { + return true; + } + + if ( !m_bLeftOwner && (pPlayer == GetThrower()) ) + { + const float flDist = CalcDistanceToAABB( + pPlayer->WorldAlignMins(), + pPlayer->WorldAlignMaxs(), + GetAbsOrigin() - pPlayer->GetAbsOrigin() ); + m_bLeftOwner = flDist > s_flClearDist; + return !m_bLeftOwner; + } + else + { + m_bLeftOwner = true; + return false; + } +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::TouchPlayer( CTFPlayer *pPlayer ) +{ + if ( !TFGameRules() ) + { + return; + } + + // + // Is this player close enough to hit it? + // TODO is this still necessary since we use actual physics touching now? + // + { + const Vector& vecMyOrigin = GetAbsOrigin(); + const Vector& vecOtherOrigin = pPlayer->GetAbsOrigin(); + const Vector vecOtherHead = vecOtherOrigin + Vector( 0, 0, pPlayer->BoundingRadius() + 8 ); + float t = 0; + const float flDist = CalcDistanceToLineSegment( vecMyOrigin, vecOtherOrigin, vecOtherHead, &t ); + if ( (flDist > s_flBlockDist) && (flDist > s_flPickupDist) ) + { + return; + } + } + + const bool bSameTeam = GetThrower() && (pPlayer->GetTeamNumber() == GetThrower()->GetTeamNumber()); + + // + // Can this player get the ball? + // + bool bCanPickUp = false; + { + HudNotification_t cantPickUpReason; + bCanPickUp = g_pPasstimeLogic->BCanPlayerPickUpBall( pPlayer, &cantPickUpReason ); + if ( cantPickUpReason ) + { + CSingleUserReliableRecipientFilter filter( pPlayer ); + TFGameRules()->SendHudNotification( filter, cantPickUpReason ); + } + } + + + if ( bCanPickUp ) + { + m_bTouchedSinceSpawn = true; + g_pPasstimeLogic->OnPlayerTouchBall( pPlayer, this ); + } + else if ( !bSameTeam ) + { + // can't pick it up and not on the same team = block + + // NOTE: BlockDamage has to come after BlockReflect in order for + // the reflection to work right. BlockDamage might apply a force + // to the player, which will taint the reflection vector. + // NOTE: because some of these functions might change the ball's + // velocity, get it once and then pass it to each. + IPhysicsObject* pPhysObj = VPhysicsGetObject(); + Vector vecBallVel; + pPhysObj->GetVelocity( &vecBallVel, 0 ); + + BlockReflect( pPlayer, pPlayer->GetAbsOrigin(), vecBallVel ); + BlockDamage( pPlayer, vecBallVel ); + + if ( GetThrower() ) + { + // ball was in flight + PasstimeGameEvents::BallBlocked( GetThrower()->entindex(), pPlayer->entindex() ).Fire(); + } + + CPasstimeBallController::DisableOn( this ); + m_iCollisionCount++; + SetThrower( 0 ); + m_flAirtimeDistance = 0; + m_flLastCollisionTime = gpGlobals->curtime; + } +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::BlockReflect( CTFPlayer *pPlayer, const Vector& vecBallOrigin, const Vector& vecBallVel ) +{ + if ( m_hBlocker == pPlayer ) + { + // this helps prevent the ball from getting stuck inside players + return; + } + + m_hBlocker = pPlayer; + + const Vector vecMyOrigin = GetAbsOrigin(); + Vector vecBallDir = vecBallVel; + vecBallDir.z = 0; + const float flBallSpeed = vecBallDir.NormalizeInPlace(); + + Vector vecReflectVel = vecMyOrigin - vecBallOrigin; + vecReflectVel.z = 0; + vecReflectVel.NormalizeInPlace(); + vecReflectVel = vecReflectVel.Cross( vecBallDir ); + vecReflectVel.NormalizeInPlace(); + vecReflectVel = vecBallDir.Cross( vecReflectVel ); + vecReflectVel.NormalizeInPlace(); + vecReflectVel -= vecBallDir; + vecReflectVel *= flBallSpeed / 2.0f; + vecReflectVel += pPlayer->GetAbsVelocity(); + + AngularImpulse spin(0,0,0); + SetAbsVelocity( vecReflectVel ); + VPhysicsGetObject()->SetVelocity( &vecReflectVel, &spin ); + + if ( flBallSpeed > 300 ) + { + EmitSound( "Passtime.BallSmack" ); + } +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::BlockDamage( CTFPlayer *pPlayer, const Vector& vecBallVel ) +{ + const float flSpeed = vecBallVel.Length(); + const float flDamageSpeed = 1000; + + pPlayer->m_Shared.OnSpyTouchedByEnemy(); + + if ( flSpeed >= flDamageSpeed ) + { + CTakeDamageInfo di; + di.SetAttacker( GetThrower() ); + di.SetDamage( 1 ); + di.SetDamageType( DMG_CLUB ); + di.SetInflictor( this ); + di.SetDamagePosition( GetAbsOrigin() ); + di.SetDamageForce( vecBallVel ); // needs to be set to nonzero + if ( flSpeed > 1200 ) + { + di.AddDamageType( DMG_CRITICAL ); + } + pPlayer->TakeDamage( di ); + } +} + +//----------------------------------------------------------------------------- +static bool IsGroundCollision( int index, const gamevcollisionevent_t *pEvent ) +{ + // this little arcane incantation stolen from somewhere else + const int otherindex = !index; + IPhysicsObject *pPhysObj = pEvent->pObjects[otherindex]; + CBaseEntity *pOther = static_cast<CBaseEntity *>(pPhysObj->GetGameData()); + + if ( !pOther || !pEvent->pInternalData ) + { + return false; // paranoia + } + + Vector vecNormal; + pEvent->pInternalData->GetSurfaceNormal( vecNormal ); + return Vector( 0, 0, 1 ).Dot( vecNormal ) < -0.7f; // why is this backwards? +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::OnTouch( CBaseEntity *pOther ) +{ + // If two players touch the ball in the same frame inside the physics system, + // the ball will get a touch callback for both regardless of what happens + // in response to the first call (i.e. it's just iterating a contact list). + // This catches the case where the ball was already picked up this frame. + if ( !TFGameRules()->IsPasstimeMode() || (m_eState != STATE_FREE) ) + { + return; + } + + CTFPlayer *pPlayer = ToTFPlayer( pOther ); + if ( !BIgnorePlayer( pPlayer ) ) + { + TouchPlayer( pPlayer ); + } +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + BaseClass::VPhysicsCollision( index, pEvent ); + + if ( !TFGameRules()->IsPasstimeMode() ) + { + return; + } + + if ( g_pPasstimeLogic && (g_pPasstimeLogic->GetBall() == this) + && g_pPasstimeLogic->OnBallCollision( this, index, pEvent ) + && IsGroundCollision( index, pEvent ) ) + { + OnCollision(); + } + CPasstimeBallController::BallCollision( this, index, pEvent ); + m_hBlocker.Term(); +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::OnCollision() +{ + m_flAirtimeDistance = 0; + m_flLastCollisionTime = gpGlobals->curtime; + ++m_iCollisionCount; + if ( m_iCollisionCount == 1 ) + { + SetThrower( 0 ); + if ( m_bTouchedSinceSpawn ) + { + SetIdleRespawnTime(); + } + } + m_hBlocker.Term(); +} + +//----------------------------------------------------------------------------- +int CPasstimeBall::OnTakeDamage( const CTakeDamageInfo &info ) +{ + if ( !tf_passtime_ball_takedamage.GetBool() ) + { + // this can happen if the cvar is disabled after the ball has spawned + return 0; + } + + if ( !m_bTouchedSinceSpawn && (GetCollisionCount() == 0) ) + { + ++CTF_GameStats.m_passtimeStats.summary.nTotalBallSpawnShots; + } + + if ( TFGameRules()->IsPasstimeMode() ) + { + CPasstimeBallController::BallDamaged( this ); + CPasstimeBallController::DisableOn( this ); + OnCollision(); + } + + if ( IPhysicsObject* pPhysObj = VPhysicsGetObject() ) + { + pPhysObj->EnableMotion( true ); + pPhysObj->ApplyForceOffset( info.GetDamageForce().Normalized() * tf_passtime_ball_takedamage_force.GetFloat(), GetAbsOrigin() ); + } + + return 0; +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::Deflected(CBaseEntity *pDeflectedBy, Vector& vecDir ) +{ + NOTE_UNUSED( pDeflectedBy ); + IPhysicsObject* pPhysObj = VPhysicsGetObject(); + if ( !pPhysObj ) + { + return; + } + + // WeaponBase::DeflectEntity will redirect the velocity with the same flSpeed, + // which means that a stationary ball won't move since it has 0 flSpeed. this + // will just make sure the velocity is what it should be + + // vecDir points from the point under the player's crosshair to the ball's origin. + // this will make ball deflection work just like rockets, except the velocity + // is normalized instead of just being whatever magnitude it was before deflection. + Vector vecVel = -vecDir * tf_passtime_ball_takedamage_force.GetFloat(); + pPhysObj->SetVelocity( &vecVel, 0 ); + + if ( TFGameRules()->IsPasstimeMode() ) + { + ++CTF_GameStats.m_passtimeStats.summary.nTotalBallDeflects; + + // stop passing, etc + CPasstimeBallController::DisableOn( this ); + + // count as a collision + OnCollision(); + } +} + +//----------------------------------------------------------------------------- +//static +CPasstimeBall *CPasstimeBall::Create( Vector vecPosition, QAngle angles ) +{ + // mostly copied from CreatePhysicsToy + MDLCACHE_CRITICAL_SECTION(); + MDLHandle_t hMdl = mdlcache->FindMDL( tf_passtime_ball_model.GetString() ); + Assert( hMdl != MDLHANDLE_INVALID ); + if( hMdl == MDLHANDLE_INVALID ) + { + return 0; + } + + studiohdr_t *pStudioHdr = mdlcache->GetStudioHdr( hMdl ); + Assert( pStudioHdr ); + if( !pStudioHdr ) + { + return 0; + } + + // i don't know what this "allow precache" stuff does, + // i copied it from other code and forgot to note where it was + bool oldAllowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + + CPasstimeBall *pBall = dynamic_cast< CPasstimeBall* >( CreateEntityByName( "passtime_ball" ) ); + + char pszBuf[512]; + Q_snprintf( pszBuf, sizeof( pszBuf ), "%.10f %.10f %.10f", vecPosition.x, vecPosition.y, vecPosition.z ); + pBall->KeyValue( "origin", pszBuf ); + Q_snprintf( pszBuf, sizeof( pszBuf ), "%.10f %.10f %.10f", angles.x, angles.y, angles.z ); + pBall->KeyValue( "angles", pszBuf ); + pBall->KeyValue( "fademindist", "-1" ); + pBall->KeyValue( "fademaxdist", "0" ); + pBall->KeyValue( "fadescale", "1" ); + DispatchSpawn( pBall ); + pBall->Activate(); + + CBaseEntity::SetAllowPrecache( oldAllowPrecache ); + mdlcache->Release( hMdl ); + return pBall; +} + +//----------------------------------------------------------------------------- +void CPasstimeBall::SetHomingTarget( CTFPlayer *pPlayer ) +{ + m_hHomingTarget = pPlayer; + if ( m_hHomingTarget ) + { + if ( !m_pBeepLoop ) + { + CReliableBroadcastRecipientFilter filter; + m_pBeepLoop = CSoundEnvelopeController::GetController().SoundCreate( + filter, entindex(), "Passtime.BallHoming" ); + CSoundEnvelopeController::GetController().Play( m_pBeepLoop, 1, PITCH_NORM ); + } + } + else + { + if ( m_pBeepLoop ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pBeepLoop ); + m_pBeepLoop = 0; + } + } +} + +//----------------------------------------------------------------------------- +CTFPlayer *CPasstimeBall::GetHomingTarget() const +{ + return m_hHomingTarget; +} + +//----------------------------------------------------------------------------- +float CPasstimeBall::GetAirtimeSec() const +{ + return MAX( 0, gpGlobals->curtime - m_flLastCollisionTime ); +} + +//----------------------------------------------------------------------------- +float CPasstimeBall::GetAirtimeDistance() const +{ + return m_flAirtimeDistance; +} + |