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