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
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(), ¶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;
|
|
}
|
|
|