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