Team Fortress 2 Source Code as on 22/4/2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1437 lines
42 KiB

//========= 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;
}