summaryrefslogtreecommitdiff
path: root/game/server/tf/tf_passtime_ball.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'game/server/tf/tf_passtime_ball.cpp')
-rw-r--r--game/server/tf/tf_passtime_ball.cpp1437
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(), &params, 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;
+}
+