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.
2312 lines
70 KiB
2312 lines
70 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
//=============================================================================// |
|
|
|
#include "cbase.h" |
|
#include "tf_passtime_logic.h" |
|
#include "countdown_announcer.h" |
|
#include "entity_passtime_ball_spawn.h" |
|
#include "func_passtime_goal.h" |
|
#include "func_passtime_no_ball_zone.h" |
|
#include "tf_passtime_ball.h" |
|
#include "passtime_ballcontroller.h" |
|
#include "passtime_convars.h" |
|
#include "passtime_game_events.h" |
|
#include "func_passtime_goalie_zone.h" |
|
#include "tf_player.h" |
|
#include "tf_team.h" |
|
#include "tf_gamestats.h" |
|
#include "tf_gamerules.h" |
|
#include "pathtrack.h" |
|
#include "tf_fx.h" |
|
#include "tf_weapon_passtime_gun.h" |
|
#include "team_objectiveresource.h" |
|
#include "mapentities.h" |
|
#include "soundenvelope.h" |
|
#include "eventqueue.h" |
|
#include "hl2orange.spa.h" // achievement defines from tf_shareddefs depend on this |
|
#include "tier0/memdbgon.h" |
|
|
|
CTFPasstimeLogic *g_pPasstimeLogic; |
|
|
|
#ifdef _DEBUG |
|
#define SECRETROOM_LOG Warning |
|
#else |
|
#define SECRETROOM_LOG (void) |
|
#endif |
|
|
|
//----------------------------------------------------------------------------- |
|
LINK_ENTITY_TO_CLASS( passtime_logic, CTFPasstimeLogic ); |
|
PRECACHE_REGISTER( passtime_logic ); |
|
IMPLEMENT_SERVERCLASS_ST( CTFPasstimeLogic, DT_TFPasstimeLogic ) |
|
SendPropEHandle( SENDINFO( m_hBall ) ), |
|
SendPropArray( SendPropVector( SENDINFO_ARRAY( m_trackPoints ), -1, SPROP_COORD_MP_INTEGRAL ), m_trackPoints ), |
|
SendPropInt( SENDINFO( m_iNumSections ) ), |
|
SendPropInt( SENDINFO( m_iCurrentSection ) ), |
|
SendPropFloat( SENDINFO( m_flMaxPassRange ) ), |
|
SendPropInt( SENDINFO( m_iBallPower ), 8 ), |
|
SendPropFloat( SENDINFO( m_flPackSpeed ) ), |
|
SendPropArray3( SENDINFO_ARRAY3( m_bPlayerIsPackMember ), SendPropInt( SENDINFO_ARRAY( m_bPlayerIsPackMember ), 1, SPROP_UNSIGNED ) ), |
|
END_SEND_TABLE() |
|
|
|
//----------------------------------------------------------------------------- |
|
BEGIN_DATADESC( CTFPasstimeLogic ) |
|
DEFINE_KEYFIELD( m_iNumSections, FIELD_INTEGER, "num_sections" ), |
|
DEFINE_KEYFIELD( m_iBallSpawnCountdownSec, FIELD_INTEGER, "ball_spawn_countdown" ), |
|
DEFINE_KEYFIELD( m_flMaxPassRange, FIELD_FLOAT, "max_pass_range" ), |
|
|
|
DEFINE_INPUTFUNC( FIELD_VOID, "SpawnBall", InputSpawnBall ), |
|
DEFINE_INPUTFUNC( FIELD_STRING, "SetSection", InputSetSection ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "TimeUp", InputTimeUp ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "SpeedBoostUsed", InputSpeedBoostUsed ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "JumpPadUsed", InputJumpPadUsed ), |
|
|
|
// secret room inputs |
|
// these strings are obfuscated for fun, not for protection |
|
DEFINE_INPUTFUNC( FIELD_VOID, "statica", statica ), // SecretRoom_InputStartTouchPlayerSlot NOTE: intentionally not in FGD |
|
DEFINE_INPUTFUNC( FIELD_VOID, "staticb", staticb ), // SecretRoom_InputEndTouchPlayerSlot NOTE: intentionally not in FGD |
|
DEFINE_INPUTFUNC( FIELD_VOID, "staticc", staticc ), // SecretRoom_InputPlugDamaged NOTE: intentionally not in FGD |
|
DEFINE_INPUTFUNC( FIELD_VOID, "RoomTriggerOnTouch", InputRoomTriggerOnTouch ), |
|
|
|
DEFINE_OUTPUT( m_onBallFree, "OnBallFree" ), |
|
DEFINE_OUTPUT( m_onBallGetBlu, "OnBallGetBlu" ), |
|
DEFINE_OUTPUT( m_onBallGetRed, "OnBallGetRed" ), |
|
DEFINE_OUTPUT( m_onBallGetAny, "OnBallGetAny" ), |
|
DEFINE_OUTPUT( m_onBallRemoved, "OnBallRemoved" ), |
|
DEFINE_OUTPUT( m_onScoreBlu, "OnScoreBlu" ), |
|
DEFINE_OUTPUT( m_onScoreRed, "OnScoreRed" ), |
|
DEFINE_OUTPUT( m_onScoreAny, "OnScoreAny" ), |
|
DEFINE_OUTPUT( m_onBallPowerUp, "OnBallPowerUp" ), |
|
DEFINE_OUTPUT( m_onBallPowerDown, "OnBallPowerDown" ), |
|
END_DATADESC() |
|
|
|
//----------------------------------------------------------------------------- |
|
static const CCountdownAnnouncer::TimeSounds sCountdownSoundsRoundBegin = { |
|
"Announcer.RoundBegins60seconds", |
|
"Announcer.RoundBegins30seconds", |
|
"Announcer.RoundBegins10seconds", |
|
"Announcer.RoundBegins5seconds", |
|
"Announcer.RoundBegins4seconds", |
|
"Announcer.RoundBegins3seconds", |
|
"Announcer.RoundBegins2seconds", |
|
"Announcer.RoundBegins1seconds", |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
static const CCountdownAnnouncer::TimeSounds sCountdownSoundsRoundBeginMerasmus = { |
|
"Announcer.RoundBegins60seconds", |
|
"Announcer.RoundBegins30seconds", |
|
"Announcer.RoundBegins10seconds", |
|
"Merasmus.RoundBegins5seconds", |
|
"Merasmus.RoundBegins4seconds", |
|
"Merasmus.RoundBegins3seconds", |
|
"Merasmus.RoundBegins2seconds", |
|
"Merasmus.RoundBegins1seconds", |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
static bool IsGamestatePlayable() |
|
{ |
|
gamerules_roundstate_t state = TFGameRules()->State_Get(); |
|
return (state == GR_STATE_RND_RUNNING) || (state == GR_STATE_STALEMATE); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
CPasstimeBall *CTFPasstimeLogic::GetBall() const { return m_hBall; } |
|
|
|
//----------------------------------------------------------------------------- |
|
CTFPasstimeLogic::CTFPasstimeLogic() |
|
{ |
|
m_SecretRoom_pTv = nullptr; |
|
m_SecretRoom_pTvSound = nullptr; |
|
m_SecretRoom_state = SecretRoomState::None; |
|
memset( m_SecretRoom_slottedPlayers, 0, sizeof( m_SecretRoom_slottedPlayers ) ); |
|
|
|
m_flNextCrowdReactionTime = 0.0f; |
|
m_nPackMemberBits = 0; |
|
m_nPrevPackMemberBits = 0; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
CTFPasstimeLogic::~CTFPasstimeLogic() |
|
{ |
|
// note: |
|
// it doesn't seem possible on the server that this destructor would be called |
|
// after a new CTFPasstimeLogic is spawned, and it's worked fine so far, but |
|
// this has been a problem in the client code. |
|
g_pPasstimeLogic = NULL; |
|
delete m_pRespawnCountdown; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::Spawn() |
|
{ |
|
g_pPasstimeLogic = this; |
|
m_iBallSpawnCountdownSec = MAX( 1, m_iBallSpawnCountdownSec ); |
|
if ( m_flMaxPassRange == 0 ) |
|
{ |
|
m_flMaxPassRange = FLT_MAX; |
|
} |
|
|
|
for ( int i = 0; i < m_bPlayerIsPackMember.Count(); ++i ) |
|
{ |
|
m_bPlayerIsPackMember.Set( i, 0 ); |
|
} |
|
|
|
for ( int i = 0; i < m_trackPoints.Count(); ++i ) |
|
{ |
|
m_trackPoints.GetForModify(i).Zero(); |
|
} |
|
|
|
const auto *pCountdownSounds = TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) |
|
? &sCountdownSoundsRoundBeginMerasmus |
|
: &sCountdownSoundsRoundBegin; |
|
m_pRespawnCountdown = new CCountdownAnnouncer( pCountdownSounds ); |
|
|
|
SetContextThink( &CTFPasstimeLogic::PostSpawn, gpGlobals->curtime, "postspawn" ); |
|
SetContextThink( &CTFPasstimeLogic::BallPower_PackHealThink, gpGlobals->curtime + 1, "packheal" ); |
|
|
|
ListenForGameEvent( "teamplay_round_stalemate" ); |
|
ListenForGameEvent( "teamplay_setup_finished" ); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose: Utility function for hooking up entity connections from code. |
|
// Would belong in BaseEnity, but this this hacky so I'm just hiding it here. |
|
//------------------------------------------------------------------------------ |
|
static CBaseEntityOutput *FindOutput( CBaseEntity *pEnt, const char *pOutputName ) |
|
{ |
|
if ( !pEnt || !pOutputName || !pOutputName[0] ) |
|
{ |
|
return nullptr; |
|
} |
|
|
|
// loop taken from ValidateEntityConnections |
|
datamap_t *dmap = pEnt->GetDataDescMap(); |
|
while ( dmap ) |
|
{ |
|
int fields = dmap->dataNumFields; |
|
for ( int i = 0; i < fields; i++ ) |
|
{ |
|
typedescription_t *dataDesc = &dmap->dataDesc[i]; |
|
if ( ( dataDesc->fieldType == FIELD_CUSTOM ) |
|
&& ( dataDesc->flags & FTYPEDESC_OUTPUT ) |
|
&& !strcmp( dataDesc->externalName, pOutputName ) ) |
|
{ |
|
return (CBaseEntityOutput *)((intptr_t)pEnt + (int)dataDesc->fieldOffset[0]); |
|
} |
|
} |
|
dmap = dmap->baseMap; |
|
} |
|
|
|
return nullptr; |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose: Utility function for hooking up entity connections from code. |
|
// Would belong in BaseEnity, but this this hacky so I'm just hiding it here. |
|
//------------------------------------------------------------------------------ |
|
static void HookOutput( const char *pSourceName, string_t pTargetName, |
|
const char *pOutputName, const char *pInputName, |
|
const char *pParameter = nullptr, int nTimesToFire = EVENT_FIRE_ALWAYS ) |
|
{ |
|
Assert( pSourceName && pSourceName[0] ); |
|
Assert( pTargetName.ToCStr() && pTargetName.ToCStr()[0] ); |
|
CBaseEntity *pEnt = gEntList.FindEntityByName( nullptr, pSourceName ); |
|
if ( !pEnt ) |
|
{ |
|
Warning( "Entity %s missing", pSourceName ); |
|
return; |
|
} |
|
|
|
CBaseEntityOutput *pOut = FindOutput( pEnt, pOutputName ); |
|
if ( !pOut ) |
|
{ |
|
Warning( "Entity %s missing output %s", pSourceName, pOutputName ); |
|
return; |
|
} |
|
|
|
CEventAction *pAction = new CEventAction(); |
|
pAction->m_iTarget = pTargetName; |
|
pAction->m_iTargetInput = AllocPooledString( pInputName ); |
|
pAction->m_nTimesToFire = nTimesToFire; |
|
pAction->m_iParameter = pParameter |
|
? AllocPooledString( pParameter ) |
|
: string_t(); |
|
pOut->AddEventAction( pAction ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::PostSpawn() |
|
{ |
|
// This can't be done in spawn because GetTeamNumber doesn't return the |
|
// correct value for any entity until after it's been Activate()d, which |
|
// happens after all the Spawns. And it can't be done in Activate() because |
|
// the order of Activation seems kinda non-deterministic. |
|
const auto &goals = CFuncPasstimeGoal::GetAutoList(); |
|
|
|
if ( ( m_iNumSections == 0 ) && ( goals.Count() == 2 ) ) |
|
{ |
|
// FIXME support > 2 goals properly |
|
CFuncPasstimeGoal *pRed = static_cast<CFuncPasstimeGoal *>( goals[0] ); |
|
CFuncPasstimeGoal *pBlu = static_cast<CFuncPasstimeGoal *>( goals[1] ); |
|
if ( pRed->GetTeamNumber() != TF_TEAM_BLUE ) // goal's color is who can score there |
|
{ |
|
V_swap( pRed, pBlu ); |
|
} |
|
m_trackPoints.Set( 0, pBlu->GetAbsOrigin() ); |
|
m_trackPoints.Set( 1, pRed->GetAbsOrigin() ); |
|
m_iNumSections = 1; |
|
} |
|
|
|
// |
|
// Determine goal type for stats |
|
// |
|
int nTotalEndzone = 0; |
|
int nTotalBasket = 0; |
|
for ( const auto *pGoalNode : goals ) |
|
{ |
|
const auto *pGoal = (const CFuncPasstimeGoal *)pGoalNode; |
|
if ( !pGoal->BDisableBallScore() ) |
|
{ |
|
++nTotalBasket; |
|
} |
|
if ( pGoal->BEnablePlayerScore() ) |
|
{ |
|
++nTotalEndzone; |
|
} |
|
} |
|
if ( nTotalBasket && !nTotalEndzone ) |
|
{ |
|
CTF_GameStats.m_passtimeStats.summary.nGoalType = 1; |
|
} |
|
else if ( !nTotalBasket && nTotalEndzone ) |
|
{ |
|
CTF_GameStats.m_passtimeStats.summary.nGoalType = 2; |
|
} |
|
else |
|
{ |
|
CTF_GameStats.m_passtimeStats.summary.nGoalType = 3; |
|
} |
|
|
|
CTF_GameStats.m_passtimeStats.summary.nRoundMaxSec = TFGameRules()->GetActiveRoundTimer()->GetTimerInitialLength(); |
|
|
|
// These used to happen from teamplay_setup_ended, but that event doesn't happen if there's no setup time |
|
// These functions should be able to determine whether or not to actually do anything based on game state |
|
SetContextThink( &CTFPasstimeLogic::BallHistSampleThink, gpGlobals->curtime, "BallHistSampleThink" ); |
|
SetContextThink( &CTFPasstimeLogic::OneSecStatsUpdateThink, gpGlobals->curtime, "OneSecStatsUpdateThink" ); |
|
|
|
BallPower_PowerThink(); |
|
BallPower_PackThink(); |
|
|
|
// secret room puzzle |
|
if ( !V_stricmp( gpGlobals->mapname.ToCStr(), "pass_brickyard" ) ) |
|
{ |
|
SecretRoom_Spawn(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
bool CTFPasstimeLogic::AddBallPower( int iPower ) |
|
{ |
|
int iThreshold = tf_passtime_powerball_threshold.GetInt(); |
|
bool bWasAboveThreshold = m_iBallPower > iThreshold; |
|
m_iBallPower = clamp( m_iBallPower + iPower, 0, 100 ); |
|
bool bIsAboveThreshold = m_iBallPower > iThreshold; |
|
if ( bWasAboveThreshold && !bIsAboveThreshold ) |
|
{ |
|
m_onBallPowerDown.FireOutput( this, this ); |
|
TFGameRules()->BroadcastSound( 255, "Powerup.Reflect.Reflect" ); |
|
return true; |
|
} |
|
else if ( !bWasAboveThreshold && bIsAboveThreshold ) |
|
{ |
|
m_onBallPowerUp.FireOutput( this, this ); |
|
TFGameRules()->BroadcastSound( 255, "Powerup.Volume.Use" ); |
|
|
|
// reschedule think so that decay stops for a while |
|
SetContextThink( &CTFPasstimeLogic::BallPower_PowerThink, |
|
gpGlobals->curtime + tf_passtime_powerball_decay_delay.GetFloat(), |
|
"BallPower_PowerThink" ); |
|
|
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::ClearBallPower() |
|
{ |
|
AddBallPower( -m_iBallPower ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::BallPower_PowerThink() |
|
{ |
|
CPasstimeBall *pBall = GetBall(); |
|
if ( !IsGamestatePlayable() || !pBall ) |
|
{ |
|
SetContextThink( &CTFPasstimeLogic::BallPower_PowerThink, |
|
gpGlobals->curtime, "BallPower_PowerThink" ); |
|
return; |
|
} |
|
|
|
float flTickTime = (pBall->GetTeamNumber() == TEAM_UNASSIGNED) |
|
? tf_passtime_powerball_decaysec_neutral.GetFloat() |
|
: tf_passtime_powerball_decaysec.GetFloat(); |
|
|
|
SetContextThink( &CTFPasstimeLogic::BallPower_PowerThink, |
|
gpGlobals->curtime + flTickTime, "BallPower_PowerThink" ); |
|
|
|
|
|
if ( !pBall->GetHomingTarget() ) |
|
{ |
|
AddBallPower( -tf_passtime_powerball_decayamount.GetInt() ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
static uint64 CalcPackMemberBits( CTFPlayer *pBallCarrier ) |
|
{ |
|
if ( !pBallCarrier || !pBallCarrier->IsAlive() ) |
|
{ |
|
return 0; |
|
} |
|
|
|
float flPackRangeSqr = tf_passtime_pack_range.GetFloat(); |
|
flPackRangeSqr *= flPackRangeSqr; |
|
int iCarrierTeam = pBallCarrier->GetTeamNumber(); |
|
Vector vecCarrierPos = pBallCarrier->GetAbsOrigin(); |
|
uint64 nNewPackMemberBits = 0; |
|
uint64 nMask = 1; |
|
for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 ) |
|
{ |
|
CTFPlayer *pPlayer = (CTFPlayer*) UTIL_PlayerByIndex( i ); |
|
|
|
// must be a valid team member within range |
|
if ( !pPlayer |
|
|| !pPlayer->IsAlive() |
|
|| ( pPlayer->GetTeamNumber() != iCarrierTeam ) |
|
|| ( pPlayer->GetAbsOrigin().DistToSqr( vecCarrierPos ) > flPackRangeSqr ) ) |
|
{ |
|
continue; |
|
} |
|
|
|
// must not be aiming (heavy spin, sniper scope, etc) |
|
if ( pPlayer->m_Shared.InCond( TF_COND_AIMING ) ) |
|
{ |
|
continue; |
|
} |
|
|
|
nNewPackMemberBits |= nMask; |
|
} |
|
return nNewPackMemberBits; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
static void SetSpeedOnFlaggedPlayers( uint64 playerBits ) |
|
{ |
|
if ( playerBits == 0 ) |
|
{ |
|
return; |
|
} |
|
|
|
uint64 nMask = 1; |
|
for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 ) |
|
{ |
|
if ( playerBits & nMask ) |
|
{ |
|
CTFPlayer *pPlayer = (CTFPlayer*)UTIL_PlayerByIndex( i ); |
|
if ( pPlayer && pPlayer->IsAlive() ) |
|
{ |
|
pPlayer->TeamFortress_SetSpeed(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::ReplicatePackMemberBits() |
|
{ |
|
uint64 nMask = 1; |
|
for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 ) |
|
{ |
|
m_bPlayerIsPackMember.Set( i, ( m_nPackMemberBits & nMask ) ? 1 : 0 ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// solo carrier is marked for death |
|
// any close teammate (2x cart distance) removes marked for death |
|
// close teammates are sped up to fastest teammate speed |
|
void CTFPasstimeLogic::BallPower_PackThink() |
|
{ |
|
SetContextThink( &CTFPasstimeLogic::BallPower_PackThink, |
|
gpGlobals->curtime, "BallPower_PackThink" ); |
|
|
|
m_flPackSpeed = 0.0f; |
|
m_nPrevPackMemberBits = m_nPackMemberBits; |
|
m_nPackMemberBits = 0; |
|
|
|
CTFPlayer *pCarrier = GetBallCarrier(); |
|
|
|
// Check if pack speed is active |
|
if ( !tf_passtime_pack_speed.GetBool() || !IsGamestatePlayable() || !pCarrier ) |
|
{ |
|
m_nPackMemberBits = 0; // redundant assignment for clarity |
|
ReplicatePackMemberBits(); |
|
SetSpeedOnFlaggedPlayers( m_nPrevPackMemberBits ); |
|
return; |
|
} |
|
|
|
// Find the pack members |
|
m_nPackMemberBits = CalcPackMemberBits( pCarrier ); |
|
ReplicatePackMemberBits(); |
|
|
|
// Find the maximum MaxSpeed of the pack |
|
bool bHasNearbyTeammate = false; |
|
float flMaxMaxSpeed = -1; |
|
uint64 nMask = 1; |
|
for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 ) |
|
{ |
|
if ( m_nPackMemberBits & nMask ) |
|
{ |
|
CTFPlayer *pPlayer = (CTFPlayer*)UTIL_PlayerByIndex( i ); |
|
if ( pPlayer && pPlayer->IsAlive() ) |
|
{ |
|
bHasNearbyTeammate = bHasNearbyTeammate || ( pPlayer != pCarrier ); |
|
flMaxMaxSpeed = MAX( flMaxMaxSpeed, pPlayer->TeamFortress_CalculateMaxSpeed() ); |
|
} |
|
} |
|
} |
|
|
|
// Apply marked for death if no teammates |
|
if ( !bHasNearbyTeammate ) |
|
{ |
|
pCarrier->m_Shared.AddCond( TF_COND_PASSTIME_PENALTY_DEBUFF, TICK_INTERVAL * 2 ); |
|
} |
|
else |
|
{ |
|
m_flPackSpeed = flMaxMaxSpeed; |
|
} |
|
|
|
// Now tell all the relevant players to refresh their maxspeed |
|
SetSpeedOnFlaggedPlayers( m_nPackMemberBits | m_nPrevPackMemberBits ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::BallPower_PackHealThink() |
|
{ |
|
SetContextThink( &CTFPasstimeLogic::BallPower_PackHealThink, gpGlobals->curtime + 1, "packheal" ); |
|
|
|
CTFPlayer *pCarrier = GetBallCarrier(); |
|
if ( !pCarrier ) |
|
{ |
|
return; |
|
} |
|
|
|
uint64 nMask = 1; |
|
uint64 nPackMemberBits = m_nPackMemberBits; |
|
float flHealAmount = tf_passtime_pack_hp_per_sec.GetFloat(); |
|
for ( int i = 1; i <= MAX_PLAYERS; ++i, nMask <<= 1 ) |
|
{ |
|
if ( ( nPackMemberBits & nMask ) == 0 ) |
|
{ |
|
continue; |
|
} |
|
|
|
CTFPlayer *pPlayer = (CTFPlayer*)UTIL_PlayerByIndex( i ); |
|
if ( !pPlayer || ( pPlayer == pCarrier ) || !pPlayer->IsAlive() ) |
|
{ |
|
continue; |
|
} |
|
|
|
int iActualHealAmount = pPlayer->TakeHealth( flHealAmount, DMG_GENERIC ); |
|
if ( iActualHealAmount <= 0 ) |
|
{ |
|
continue; |
|
} |
|
|
|
// I'm abusing the player_healonhit event because it does the visual fx I want |
|
IGameEvent *pEvent = gameeventmanager->CreateEvent( "player_healonhit" ); |
|
if ( pEvent ) |
|
{ |
|
pEvent->SetInt( "amount", iActualHealAmount ); |
|
pEvent->SetInt( "entindex", pPlayer->entindex() ); |
|
gameeventmanager->FireEvent( pEvent ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
float CTFPasstimeLogic::GetPackSpeed( CTFPlayer *pPlayer ) const |
|
{ |
|
if ( pPlayer ) |
|
{ |
|
uint64 nMask = (uint64)1 << ( pPlayer->entindex() - 1 ); |
|
if ( m_nPackMemberBits & nMask ) |
|
{ |
|
return m_flPackSpeed; |
|
} |
|
} |
|
return 0; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::FireGameEvent( IGameEvent *pEvent ) |
|
{ |
|
const char *pEventName = pEvent->GetName(); |
|
if ( !V_strcmp( pEventName, "teamplay_round_stalemate" ) ) |
|
{ |
|
// this only happens when mp_stalemate_enable is on |
|
CTF_GameStats.m_passtimeStats.summary.nRoundMaxSec = |
|
TFGameRules()->GetActiveRoundTimer()->GetTimeRemaining(); |
|
RespawnBall(); |
|
} |
|
else if ( !V_strcmp( pEventName, "teamplay_setup_finished" ) ) |
|
{ |
|
// respawn the ball even though it already exists so that it doesn't |
|
// catch any rotation from any spawn box it might be sitting in that |
|
// hasn't opened yet. |
|
SpawnBallAtRandomSpawner(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// FIXME copypasta with tf_hud_passtime.cpp |
|
// For stats |
|
float CTFPasstimeLogic::CalcProgressFrac() const |
|
{ |
|
if ( !GetBall() || (m_iNumSections == 0) ) |
|
{ |
|
return 0; |
|
} |
|
|
|
// |
|
// Which point are we trying to classify? |
|
// |
|
Vector vecOrigin; |
|
{ |
|
CPasstimeBall *pBall = GetBall(); |
|
CTFPlayer *pCarrier = pBall->GetCarrier(); |
|
vecOrigin = pCarrier |
|
? pCarrier->GetAbsOrigin() |
|
: pBall->GetAbsOrigin(); |
|
} |
|
|
|
// |
|
// Find distance along track from first goal to last goal |
|
// |
|
float flBestLen = 0; |
|
float flTotalLen = 1; // don't set 0 so div by zero is impossible |
|
{ |
|
float flBestDist = FLT_MAX; |
|
|
|
Vector vecThisPoint; |
|
Vector vecPointOnLine; |
|
Vector vecPrevPoint = m_trackPoints[0]; |
|
float flThisFrac = 0; |
|
float flThisLen = 0; |
|
float flThisDist = 0; |
|
for ( int i = 1; i < 16; ++i ) |
|
{ |
|
vecThisPoint = m_trackPoints[i]; |
|
if ( vecThisPoint.IsZero() ) |
|
{ |
|
break; |
|
} |
|
flThisLen = (vecThisPoint - vecPrevPoint).Length(); |
|
flTotalLen += flThisLen; |
|
CalcClosestPointOnLineSegment( vecOrigin, vecPrevPoint, vecThisPoint, vecPointOnLine, &flThisFrac ); |
|
flThisDist = (vecPointOnLine - vecOrigin).Length(); |
|
if ( flThisDist < flBestDist ) |
|
{ |
|
flBestDist = flThisDist; |
|
flBestLen = flTotalLen - (flThisLen * (1.0f - flThisFrac)); |
|
} |
|
vecPrevPoint = vecThisPoint; |
|
} |
|
} |
|
|
|
return (float)(flBestLen / flTotalLen); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::BallHistSampleThink() |
|
{ |
|
CPasstimeBall *pBall = m_hBall; |
|
if ( IsGamestatePlayable() && pBall && !pBall->BOutOfPlay() ) |
|
{ |
|
CTF_GameStats.m_passtimeStats.AddBallFracSample( CalcProgressFrac() ); |
|
} |
|
|
|
SetContextThink( &CTFPasstimeLogic::BallHistSampleThink, gpGlobals->curtime + 0.125f, "BallHistSampleThink" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::OneSecStatsUpdateThink() |
|
{ |
|
CTFTeam *pBlue = GetGlobalTFTeam( TF_TEAM_BLUE ); |
|
CTFTeam *pRed = GetGlobalTFTeam( TF_TEAM_RED ); |
|
|
|
// FIXME this is a hack but it'll work for now |
|
CTF_GameStats.m_passtimeStats.summary.nPlayersBlueMax = MAX( CTF_GameStats.m_passtimeStats.summary.nPlayersBlueMax, pBlue->GetNumPlayers() ); |
|
CTF_GameStats.m_passtimeStats.summary.nPlayersRedMax = MAX( CTF_GameStats.m_passtimeStats.summary.nPlayersRedMax, pRed->GetNumPlayers() ); |
|
|
|
SetContextThink( &CTFPasstimeLogic::OneSecStatsUpdateThink, gpGlobals->curtime + 1, "OneSecStatsUpdateThink" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
static void MapEventStat( CBaseEntity *pActivator, CPasstimeBall *pBall, int *pTotal, int *pCarrierTotal ) |
|
{ |
|
CTFPlayer *pPlayer = ToTFPlayer( pActivator ); |
|
if ( pPlayer ) |
|
{ |
|
++(*pTotal); |
|
if ( pBall && (pBall->GetCarrier() == pPlayer) ) |
|
{ |
|
++(*pCarrierTotal); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
CTFPlayer *CTFPasstimeLogic::GetBallCarrier() const |
|
{ |
|
const CPasstimeBall *pBall = m_hBall.Get(); |
|
if ( !pBall ) |
|
{ |
|
return nullptr; |
|
} |
|
return pBall->GetCarrier(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::InputSpeedBoostUsed( inputdata_t &input ) |
|
{ |
|
MapEventStat( input.pActivator, GetBall(), |
|
&CTF_GameStats.m_passtimeStats.summary.nTotalSpeedBoosts, |
|
&CTF_GameStats.m_passtimeStats.summary.nTotalCarrierSpeedBoosts ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::InputJumpPadUsed( inputdata_t &input ) |
|
{ |
|
MapEventStat( input.pActivator, GetBall(), |
|
&CTF_GameStats.m_passtimeStats.summary.nTotalJumpPads, |
|
&CTF_GameStats.m_passtimeStats.summary.nTotalCarrierJumpPads ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::Precache() |
|
{ |
|
PrecacheScriptSound( "Passtime.BallIntercepted" ); |
|
PrecacheScriptSound( "Passtime.BallStolen" ); |
|
PrecacheScriptSound( "Passtime.BallDropped" ); |
|
PrecacheScriptSound( "Passtime.BallCatch" ); |
|
PrecacheScriptSound( "Passtime.BallSpawn" ); |
|
|
|
PrecacheScriptSound( "Passtime.Crowd.Boo" ); |
|
PrecacheScriptSound( "Passtime.Crowd.Cheer" ); |
|
PrecacheScriptSound( "Passtime.Crowd.React.Neg" ); |
|
PrecacheScriptSound( "Passtime.Crowd.React.Pos" ); |
|
|
|
PrecacheScriptSound( "Powerup.Reflect.Reflect" ); // for powerball |
|
PrecacheScriptSound( "Powerup.Volume.Use" ); |
|
|
|
PrecacheScriptSound( "Announcer.RoundBegins60seconds"); |
|
PrecacheScriptSound( "Announcer.RoundBegins30seconds"); |
|
PrecacheScriptSound( "Announcer.RoundBegins10seconds"); |
|
|
|
if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) ) |
|
{ |
|
PrecacheScriptSound( "Merasmus.RoundBegins5seconds"); |
|
PrecacheScriptSound( "Merasmus.RoundBegins4seconds"); |
|
PrecacheScriptSound( "Merasmus.RoundBegins3seconds"); |
|
PrecacheScriptSound( "Merasmus.RoundBegins2seconds"); |
|
PrecacheScriptSound( "Merasmus.RoundBegins1seconds"); |
|
|
|
PrecacheScriptSound( "sf14.Merasmus.Soccer.GoalRed" ); |
|
PrecacheScriptSound( "sf14.Merasmus.Soccer.GoalBlue" ); |
|
PrecacheScriptSound( "Passtime.Merasmus.Laugh" ); |
|
} |
|
else |
|
{ |
|
PrecacheScriptSound( "Announcer.RoundBegins5seconds"); |
|
PrecacheScriptSound( "Announcer.RoundBegins4seconds"); |
|
PrecacheScriptSound( "Announcer.RoundBegins3seconds"); |
|
PrecacheScriptSound( "Announcer.RoundBegins2seconds"); |
|
PrecacheScriptSound( "Announcer.RoundBegins1seconds"); |
|
} |
|
|
|
PrecacheScriptSound( "Game.Overtime"); |
|
PrecacheScriptSound( "Passtime.AskForBall" ); |
|
|
|
// secret room stuff |
|
if ( !V_stricmp( gpGlobals->mapname.ToCStr(), "pass_brickyard" ) ) |
|
{ |
|
PrecacheScriptSound( "Passtime.Tv1" ); |
|
PrecacheScriptSound( "Passtime.Tv2" ); |
|
PrecacheScriptSound( "Passtime.Tv3" ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::OnEnterGoal( CPasstimeBall *pBall, CFuncPasstimeGoal *pGoal ) |
|
{ |
|
if ( pGoal->BDisableBallScore() || !IsGamestatePlayable() ) |
|
{ |
|
return; |
|
} |
|
|
|
// -1 iPoints is a special hacked value that means "kill zone" |
|
if ( pGoal->Points() == -1 ) |
|
{ |
|
m_onBallRemoved.FireOutput( pGoal, pGoal ); |
|
SetContextThink( &CTFPasstimeLogic::RespawnBall, gpGlobals->curtime, "spawnball" ); |
|
return; |
|
} |
|
|
|
if ( (pBall->GetCollisionCount() > 0) || (pBall->GetTeamNumber() == TEAM_UNASSIGNED) ) |
|
{ |
|
return; |
|
} |
|
|
|
CTFPlayer *pOwner = pBall->GetThrower(); |
|
if ( pOwner && (pBall->GetTeamNumber() == pGoal->GetTeamNumber()) ) |
|
{ |
|
Score( pBall, pGoal ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::OnEnterGoal( CTFPlayer *pPlayer, CFuncPasstimeGoal *pGoal ) |
|
{ |
|
if ( IsGamestatePlayable() |
|
&& pGoal->BEnablePlayerScore() |
|
&& pPlayer->m_Shared.HasPasstimeBall() |
|
&& (pPlayer->GetTeamNumber() == pGoal->GetTeamNumber()) ) |
|
{ |
|
Score( pPlayer, pGoal ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::OnExitGoal( CPasstimeBall *pBall, CFuncPasstimeGoal *pGoal ) |
|
{ |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::OnStayInGoal( CTFPlayer *pPlayer, CFuncPasstimeGoal *pGoal ) |
|
{ |
|
OnEnterGoal( pPlayer, pGoal ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
bool CTFPasstimeLogic::OnBallCollision( CPasstimeBall *pBall, int index, gamevcollisionevent_t *pEvent ) |
|
{ |
|
if ( !IsGamestatePlayable() ) |
|
{ |
|
return false; |
|
} |
|
|
|
// FIXME |
|
//if ( pBall && pBall->BAnyControllerApplied() ) |
|
//{ |
|
// return false; |
|
//} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
bool CTFPasstimeLogic::BCanPlayerPickUpBall( CTFPlayer *pPlayer, HudNotification_t *pReason ) const |
|
{ |
|
if ( pReason ) *pReason = (HudNotification_t) 0; |
|
|
|
const auto *pBall = m_hBall.Get(); |
|
if ( !pBall ) |
|
{ |
|
return false; |
|
} |
|
|
|
if ( !pPlayer || !IsGamestatePlayable() ) |
|
{ |
|
return false; |
|
} |
|
|
|
if ( pPlayer->m_Shared.InCond( TF_COND_INVULNERABLE ) |
|
|| pPlayer->m_Shared.InCond( TF_COND_PHASE ) |
|
|| pPlayer->m_Shared.InCond( TF_COND_INVULNERABLE_WEARINGOFF ) ) |
|
{ |
|
if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_INVULN; |
|
return false; |
|
} |
|
|
|
if ( pPlayer->m_Shared.IsStealthed() ) |
|
{ |
|
if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_CLOAK; |
|
return false; |
|
} |
|
|
|
// let disguised spies pick up enemy ball, which amounts to interception and fake passes |
|
auto bEnemyBall = ( pBall->GetTeamNumber() != TEAM_UNASSIGNED ) |
|
&& ( pBall->GetTeamNumber() != pPlayer->GetTeamNumber() ); |
|
if ( pPlayer->m_Shared.InCond( TF_COND_DISGUISED ) && !bEnemyBall ) |
|
{ |
|
if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_DISGUISE; |
|
return false; |
|
} |
|
|
|
if ( pPlayer->m_Shared.IsCarryingObject() ) |
|
{ |
|
if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_CARRY; |
|
return false; |
|
} |
|
|
|
if ( pPlayer->m_Shared.InCond( TF_COND_SELECTED_TO_TELEPORT ) ) |
|
{ |
|
if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_TELE; |
|
return false; |
|
} |
|
|
|
if ( pPlayer->IsTaunting() ) |
|
{ |
|
if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_TAUNT; |
|
return false; |
|
} |
|
|
|
if ( !pPlayer->IsAllowedToPickUpFlag() |
|
|| !pPlayer->IsAlive() // NOTE: it's possible to be !alive and !dead at the same time |
|
|| pPlayer->IsAwayFromKeyboard() |
|
|| pPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) |
|
|| pPlayer->m_Shared.IsControlStunned() ) |
|
{ |
|
return false; |
|
} |
|
|
|
if ( pPlayer->m_bIsTeleportingUsingEurekaEffect ) |
|
{ |
|
if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_TELE; |
|
return false; |
|
} |
|
|
|
CTFWeaponBase *pActiveWeapon = pPlayer->GetActiveTFWeapon(); |
|
if ( pActiveWeapon ) |
|
{ |
|
bool bCanHolster = pActiveWeapon->CanHolster() |
|
&& !( pActiveWeapon->IsReloading() && pActiveWeapon->ReloadsSingly() && pActiveWeapon->CanOverload() ); // semihack to fix beggars bazooka problems |
|
if ( pActiveWeapon && ( pActiveWeapon->GetWeaponID() != TF_WEAPON_PASSTIME_GUN ) && !bCanHolster ) |
|
{ |
|
if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_HOLSTER; |
|
return false; |
|
} |
|
} |
|
|
|
if ( EntityIsInNoBallZone( pPlayer ) ) |
|
{ |
|
if ( pReason ) *pReason = HUD_NOTIFY_PASSTIME_NO_OOB; |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
int CTFPasstimeLogic::UpdateTransmitState() |
|
{ |
|
return SetTransmitState( FL_EDICT_ALWAYS ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::RespawnBall() |
|
{ |
|
Assert( m_hBall ); |
|
if ( !m_hBall ) // paranoia |
|
{ |
|
return; |
|
} |
|
|
|
ClearBallPower(); |
|
|
|
// TFGameRules only checks capture limit once per second, so this code can't rely on game state changing |
|
int iScoreLimit = tf_passtime_scores_per_round.GetInt(); |
|
bool bGameOver = ( TFTeamMgr()->GetFlagCaptures( TF_TEAM_RED ) >= iScoreLimit ) |
|
|| ( TFTeamMgr()->GetFlagCaptures( TF_TEAM_BLUE ) >= iScoreLimit ); |
|
|
|
gamerules_roundstate_t state = TFGameRules()->State_Get(); |
|
if ( bGameOver || (state == GR_STATE_GAME_OVER) || (state == GR_STATE_TEAM_WIN) || (state == GR_STATE_RESTART) ) |
|
{ |
|
m_hBall->SetStateOutOfPlay(); |
|
MoveBallToSpawner(); |
|
} |
|
else if ( (state == GR_STATE_RND_RUNNING) || (state == GR_STATE_STALEMATE) ) |
|
{ |
|
// TODO just end the game if there's not enough time to respawn the ball |
|
m_hBall->SetStateOutOfPlay(); |
|
MoveBallToSpawner(); |
|
CTeamRoundTimer *pTimer = TFGameRules()->GetActiveRoundTimer(); |
|
if ( !pTimer || ( pTimer->GetTimeRemaining() > m_iBallSpawnCountdownSec ) ) |
|
{ |
|
m_pRespawnCountdown->Start( m_iBallSpawnCountdownSec ); |
|
SpawnBallAtRandomSpawnerThink(); |
|
} |
|
} |
|
else // pre-round etc |
|
{ |
|
SpawnBallAtRandomSpawner(); |
|
} |
|
|
|
m_ballLastHeldTimes.RemoveAll(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::SpawnBallAtRandomSpawnerThink() |
|
{ |
|
if ( TFGameRules()->State_Get() == GR_STATE_GAME_OVER ) |
|
{ |
|
m_hBall->SetStateOutOfPlay(); |
|
m_pRespawnCountdown->Disable(); |
|
} |
|
else if ( m_pRespawnCountdown->Tick( 1 ) ) |
|
{ |
|
SpawnBallAtRandomSpawner(); |
|
} |
|
else |
|
{ |
|
SetContextThink( &CTFPasstimeLogic::SpawnBallAtRandomSpawnerThink, gpGlobals->curtime + 1, "spawnball" ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::SpawnBallAtRandomSpawner() |
|
{ |
|
const auto &allSpawns = IPasstimeBallSpawnAutoList::AutoList(); |
|
int i = RandomInt( 0, allSpawns.Count() - 1 ); |
|
CPasstimeBallSpawn *pSpawner = static_cast< CPasstimeBallSpawn *>( allSpawns[i] ); |
|
SpawnBallAtSpawner( pSpawner ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::MoveBallToSpawner() |
|
{ |
|
const auto &allSpawns = IPasstimeBallSpawnAutoList::AutoList(); |
|
int i = RandomInt( 0, allSpawns.Count() - 1 ); |
|
CPasstimeBallSpawn *pSpawner = static_cast< CPasstimeBallSpawn *>( allSpawns[i] ); |
|
m_hBall->MoveTo( pSpawner->GetAbsOrigin(), Vector( 0, 0, 0 ) ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::SpawnBallAtSpawner( CPasstimeBallSpawn *pSpawner ) |
|
{ |
|
if ( !m_hBall ) |
|
{ |
|
// NOTE: this is the first place where the ball is created - on first spawn |
|
m_hBall = CPasstimeBall::Create( pSpawner->GetAbsOrigin(), QAngle(0,0,0) ); |
|
} |
|
|
|
StopAskForBallEffects(); |
|
m_hBall->SetStateFree(); |
|
m_hBall->MoveToSpawner( pSpawner->GetAbsOrigin() ); |
|
m_hBall->ChangeTeam( pSpawner->GetTeamNumber() ); |
|
m_onBallFree.FireOutput( m_hBall, this ); |
|
pSpawner->m_onSpawnBall.FireOutput( pSpawner, pSpawner ); |
|
|
|
TFGameRules()->BroadcastSound( 255, "Passtime.BallSpawn" ); |
|
if ( TFGameRules()->IsHolidayActive( kHoliday_Halloween ) ) |
|
{ |
|
TFGameRules()->BroadcastSound( 255, "Passtime.Merasmus.Laugh" ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::StopAskForBallEffects() |
|
{ |
|
for( int i = 1; i <= MAX_PLAYERS; i++ ) |
|
{ |
|
CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); |
|
if ( pPlayer ) |
|
{ |
|
pPlayer->m_Shared.SetAskForBallTime( 0 ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::OnBallCarrierMeleeHit( CTFPlayer *pPlayer, CTFPlayer *pAttacker ) |
|
{ |
|
// TODO refactor OnBallCarrierMeleeHit and OnBallCarrierDamaged for less copypasta |
|
|
|
if ( !pPlayer || !pAttacker || (pPlayer == pAttacker) || !TFGameRules() ) |
|
{ |
|
// shouldn't happen, but who knows |
|
return; |
|
} |
|
|
|
Assert( pPlayer->m_Shared.HasPasstimeBall() ); |
|
if ( !pPlayer->m_Shared.HasPasstimeBall() ) |
|
{ |
|
return; |
|
} |
|
|
|
if ( !pPlayer->InSameTeam( pAttacker) ) |
|
{ |
|
// currently handled by OnBallCarrierDamaged |
|
return; |
|
} |
|
|
|
Assert( m_hBall ); |
|
if( !m_hBall ) |
|
{ |
|
return; |
|
} |
|
|
|
bool bTooLong = (m_hBall->GetCarryDuration() > tf_passtime_teammate_steal_time.GetFloat()); |
|
if ( pPlayer->m_bPasstimeBallSlippery || bTooLong ) |
|
{ |
|
// once a player has held the ball too long, mark them as a jerk |
|
// so they can't hoard the ball ever again |
|
StealBall( pPlayer, pAttacker ); |
|
pPlayer->m_bPasstimeBallSlippery = true; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::OnBallCarrierDamaged( CTFPlayer *pPlayer, CTFPlayer *pAttacker, |
|
const CTakeDamageInfo& info ) |
|
{ |
|
// TODO refactor OnBallCarrierMeleeHit and OnBallCarrierDamaged for less copypasta |
|
|
|
// NOTE: it's possible that neither player has the ball if the attacker |
|
// killed the carrier, which would cause EjectBall to happen before |
|
// this call. There's no good way around it. |
|
|
|
if ( !pPlayer || !pAttacker || (pPlayer == pAttacker) || !TFGameRules() ) |
|
{ |
|
// happens from world damage |
|
return; |
|
} |
|
|
|
// |
|
// Only care about melee damage |
|
// |
|
// DMG_CLUB is demo charge |
|
if ( !tf_passtime_steal_on_melee.GetBool() || !(info.GetDamageType() & (DMG_MELEE | DMG_CLUB)) ) |
|
{ |
|
return; |
|
} |
|
|
|
Assert( m_hBall ); |
|
if ( !m_hBall ) |
|
{ |
|
return; |
|
} |
|
|
|
if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BASEBALL ) |
|
{ |
|
auto launch = CPasstimeGun::CalcLaunch( pPlayer, false ); |
|
LaunchBall(pPlayer, launch.startPos, launch.startVel ); |
|
} |
|
else |
|
{ |
|
StealBall( pPlayer, pAttacker ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::CrowdReactionSound( int iTeam ) |
|
{ |
|
if ( m_flNextCrowdReactionTime <= gpGlobals->curtime ) |
|
{ |
|
TFGameRules()->BroadcastSound( iTeam, "Passtime.Crowd.React.Pos" ); |
|
TFGameRules()->BroadcastSound( GetEnemyTeam( iTeam ), "Passtime.Crowd.React.Neg" ); |
|
m_flNextCrowdReactionTime = gpGlobals->curtime + 10.0f; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::StealBall( CTFPlayer *pFrom, CTFPlayer *pTo ) |
|
{ |
|
if ( pFrom->m_Shared.HasPasstimeBall() ) |
|
{ |
|
EjectBall( pFrom, pTo ); |
|
} |
|
|
|
HudNotification_t cantPickUpReason; |
|
if ( BCanPlayerPickUpBall( pTo, &cantPickUpReason ) ) |
|
{ |
|
if ( !pFrom->m_bPasstimeBallSlippery ) |
|
{ |
|
CTF_GameStats.Event_PlayerAwardBonusPoints( pTo, pTo, 10 ); |
|
} |
|
|
|
TFGameRules()->BroadcastSound( 255, "Passtime.BallStolen" ); |
|
|
|
++CTF_GameStats.m_passtimeStats.summary.nTotalSteals; |
|
|
|
m_hBall->SetStateCarried( pTo ); |
|
OnBallGet(); |
|
pTo->m_Shared.AddCond( TF_COND_PASSTIME_INTERCEPTION, tf_passtime_speedboost_on_get_ball_time.GetFloat() ); |
|
|
|
int pointsToAward = 5; |
|
if ( CFuncPasstimeGoalieZone::BPlayerInAny( pTo ) ) |
|
{ |
|
++CTF_GameStats.m_passtimeStats.summary.nTotalStealsNearGoal; |
|
pointsToAward = 10; // Extra points for last second defend. |
|
} |
|
|
|
if ( !pFrom->m_bPasstimeBallSlippery ) |
|
{ |
|
CTF_GameStats.Event_PlayerAwardBonusPoints( pTo, 0, pointsToAward ); |
|
} |
|
|
|
PasstimeGameEvents::BallStolen( pFrom->entindex(), pTo->entindex() ).Fire(); |
|
CrowdReactionSound( pTo->GetTeamNumber() ); |
|
} |
|
else if ( cantPickUpReason ) |
|
{ |
|
CSingleUserReliableRecipientFilter filter( pTo ); |
|
TFGameRules()->SendHudNotification( filter, cantPickUpReason ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
float CTFPasstimeLogic::GetLastHeldTime( CTFPlayer* pPlayer ) |
|
{ |
|
float lastHeldTime = 0.0f; |
|
for ( int i = 0; i < m_ballLastHeldTimes.Count(); i++ ) |
|
{ |
|
if ( m_ballLastHeldTimes[i].first == pPlayer ) |
|
{ |
|
lastHeldTime = m_ballLastHeldTimes[i].second; |
|
break; |
|
} |
|
} |
|
|
|
return lastHeldTime; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
float CTFPasstimeLogic::GetLastPassTime( CTFPlayer* pPlayer ) |
|
{ |
|
float lastPassTime = 0.0f; |
|
for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ ) |
|
{ |
|
if ( m_ballLastPassTimes[i].first == pPlayer ) |
|
{ |
|
lastPassTime = m_ballLastPassTimes[i].second; |
|
break; |
|
} |
|
} |
|
|
|
return lastPassTime; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::SetLastPassTime( CTFPlayer* pPlayer ) |
|
{ |
|
std::pair<CTFPlayer*, float> toAdd( pPlayer, gpGlobals->realtime ); |
|
bool skipTheRest = false; |
|
for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ ) |
|
{ |
|
if ( m_ballLastPassTimes[i].first == pPlayer ) |
|
{ |
|
m_ballLastPassTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector |
|
skipTheRest = true; |
|
break; |
|
} |
|
} |
|
|
|
if ( !skipTheRest ) |
|
{ |
|
m_ballLastPassTimes.AddToTail( toAdd ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::EjectBall( CTFPlayer *pPlayer, CTFPlayer *pAttacker ) |
|
{ |
|
if ( !m_hBall ) |
|
{ |
|
// I'm not sure how this is possible, but if I'm recording with hltv in |
|
// a listen server that has bots in it (which requires a hack in the bot |
|
// concommand) and I restart the game while a bot is holding the ball... |
|
// then m_hBall is invalid. |
|
if ( pPlayer ) |
|
{ |
|
// This has to be true to get into this function for the case I just |
|
// described above, and since the ball has been deleted somehow during |
|
// the round restart, it probably isn't necessary to set this to 0 |
|
// because the player's going to be reset anyway. But I want to make |
|
// sure it's correct. |
|
pPlayer->m_Shared.SetHasPasstimeBall( 0 ); |
|
} |
|
return; |
|
} |
|
|
|
m_hBall->SetStateFree(); |
|
m_hBall->ChangeTeam( TEAM_UNASSIGNED ); |
|
|
|
Vector vecEjectVel( 0, 0, 600 ); |
|
vecEjectVel += pPlayer->GetAbsVelocity() * 0.1f; |
|
m_hBall->MoveTo( pPlayer->GetAbsOrigin() + Vector( 0, 0, 32 ), vecEjectVel ); |
|
if ( pPlayer != pAttacker ) |
|
{ |
|
pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_LOST_OBJECT ); |
|
pAttacker->SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_TAUNTS ); |
|
} |
|
|
|
m_onBallFree.FireOutput( m_hBall, this ); |
|
std::pair<CTFPlayer*, float> toAdd( pPlayer, gpGlobals->realtime ); |
|
for ( int i = 0; i < m_ballLastHeldTimes.Count(); i++ ) |
|
{ |
|
if ( m_ballLastHeldTimes[i].first == pPlayer ) |
|
{ |
|
m_ballLastHeldTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector |
|
return; |
|
} |
|
} |
|
|
|
m_ballLastHeldTimes.AddToTail( toAdd ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::LaunchBall( CTFPlayer *pPlayer, const Vector &vecPos, const Vector &vecVel ) |
|
{ |
|
StopAskForBallEffects(); |
|
m_hBall->SetStateFree(); |
|
m_hBall->MoveTo( vecPos, vecVel ); |
|
m_onBallFree.FireOutput( m_hBall, this ); |
|
std::pair<CTFPlayer*, float> toAdd( pPlayer, gpGlobals->realtime ); |
|
for ( int i = 0; i < m_ballLastHeldTimes.Count(); i++ ) |
|
{ |
|
if ( m_ballLastHeldTimes[i].first == pPlayer ) |
|
{ |
|
m_ballLastHeldTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector |
|
return; |
|
} |
|
} |
|
|
|
m_ballLastHeldTimes.AddToTail( toAdd ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::Score( CTFPlayer *pPlayer, CFuncPasstimeGoal *pGoal ) |
|
{ |
|
Assert( pPlayer && pGoal ); |
|
pGoal->OnScore( pPlayer->GetTeamNumber() ); |
|
Score( pPlayer, pGoal->GetTeamNumber(), pGoal->Points(), pGoal->BWinOnScore() ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::Score( CPasstimeBall *pBall, CFuncPasstimeGoal *pGoal ) |
|
{ |
|
Assert( pBall && pGoal ); |
|
CTFPlayer* pPlayer = pBall->GetThrower(); |
|
Assert( pPlayer ); |
|
pGoal->OnScore( pPlayer->GetTeamNumber() ); |
|
Score( pPlayer, pGoal->GetTeamNumber(), pGoal->Points(), pGoal->BWinOnScore() ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// static |
|
void CTFPasstimeLogic::AddCondToTeam( ETFCond eCond, int iTeam, float flTime ) |
|
{ |
|
for ( int i = 1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); |
|
if ( pTFPlayer && (pTFPlayer->GetTeamNumber() == iTeam) && pTFPlayer->IsAlive() ) |
|
{ |
|
pTFPlayer->m_Shared.AddCond( eCond, flTime ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::Score( CTFPlayer *pPlayer, int iTeam, int iPoints, bool bForceWin ) |
|
{ |
|
StopAskForBallEffects(); |
|
m_pRespawnCountdown->Disable(); |
|
|
|
Assert( pPlayer ); |
|
if ( !pPlayer || ( iTeam == TEAM_UNASSIGNED ) ) |
|
{ |
|
return; |
|
} |
|
|
|
if ( bForceWin ) |
|
{ |
|
iPoints = MAX( 1, tf_passtime_scores_per_round.GetInt() - TFTeamMgr()->GetFlagCaptures( iTeam ) ); |
|
} |
|
|
|
// |
|
// Update stats |
|
// |
|
++CTF_GameStats.m_passtimeStats.summary.nTotalScores; |
|
++CTF_GameStats.m_passtimeStats.classes[ pPlayer->GetPlayerClass()->GetClassIndex() ].nTotalScores; |
|
CTF_GameStats.Event_PlayerCapturedPoint( pPlayer ); |
|
|
|
// |
|
// Award player points |
|
// |
|
CTF_GameStats.Event_PlayerAwardBonusPoints( pPlayer, 0, 25 ); |
|
|
|
// |
|
// Award player assist points |
|
// |
|
{ |
|
CTFPlayer *pAssister = nullptr; |
|
float flAssisterTime = FLT_MAX; |
|
for ( unsigned short i = 0; i < m_ballLastHeldTimes.Count(); i++ ) |
|
{ |
|
auto &tempPair = m_ballLastHeldTimes[i]; |
|
auto *pPossibleAssister = tempPair.first; |
|
auto timeLastHeld = tempPair.second; |
|
auto flSecAgo = gpGlobals->realtime - timeLastHeld; |
|
if ( ( pPossibleAssister->GetTeamNumber() == pPlayer->GetTeamNumber() ) |
|
&& ( pPossibleAssister != pPlayer ) |
|
&& ( flSecAgo < 10.0f ) |
|
&& ( flSecAgo < flAssisterTime ) ) |
|
{ |
|
pAssister = pPossibleAssister; |
|
flAssisterTime = flSecAgo; |
|
} |
|
} |
|
|
|
if ( pAssister ) |
|
{ |
|
CTF_GameStats.Event_PlayerAwardBonusPoints( pAssister, 0, 10 ); |
|
PasstimeGameEvents::Score( pPlayer->entindex(), pAssister->entindex(), iPoints ).Fire(); |
|
} |
|
else |
|
{ |
|
PasstimeGameEvents::Score( pPlayer->entindex(), iPoints ).Fire(); |
|
} |
|
} |
|
|
|
// |
|
// Award team points |
|
// |
|
while ( iPoints-- > 0 ) |
|
{ |
|
TFTeamMgr()->IncrementFlagCaptures( iTeam ); |
|
} |
|
|
|
// |
|
// Award bonus conditions |
|
// |
|
AddCondToTeam( TF_COND_CRITBOOSTED_CTF_CAPTURE, pPlayer->GetTeamNumber(), tf_passtime_score_crit_sec.GetFloat() ); |
|
|
|
// |
|
// Feedback |
|
// |
|
pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_FLAGCAPTURED ); |
|
TFGameRules()->BroadcastSound( iTeam, "Passtime.Crowd.Cheer" ); |
|
TFGameRules()->BroadcastSound( GetEnemyTeam( iTeam ), "Passtime.Crowd.Boo" ); |
|
|
|
if ( TFGameRules()->IsHolidayActive( kHoliday_Halloween ) ) |
|
{ |
|
const char* pszSound = ( iTeam == TF_TEAM_RED ) |
|
? "sf14.Merasmus.Soccer.GoalRed" |
|
: "sf14.Merasmus.Soccer.GoalBlue"; |
|
TFGameRules()->BroadcastSound( 255, pszSound ); |
|
} |
|
|
|
// |
|
// Game state management |
|
// |
|
ClearBallPower(); |
|
m_hBall->SetStateOutOfPlay(); |
|
MoveBallToSpawner(); // move it now instead of when it spawns to avoid lerping |
|
|
|
// |
|
// Finish round or respawn ball |
|
// |
|
CTeamRoundTimer *pRoundTimer = TFGameRules()->GetActiveRoundTimer(); |
|
if ( ( TFGameRules()->State_Get() == GR_STATE_STALEMATE ) || ( pRoundTimer && ( pRoundTimer->GetTimeRemaining() <= 0.0f ) ) ) |
|
{ |
|
EndRoundExpiredTimer(); |
|
} |
|
else |
|
{ |
|
SetContextThink( &CTFPasstimeLogic::RespawnBall, gpGlobals->curtime, "spawnball" ); |
|
} |
|
|
|
// |
|
// Fire outputs |
|
// |
|
m_onScoreAny.FireOutput( pPlayer, this ); |
|
if( iTeam == TF_TEAM_RED ) |
|
{ |
|
m_onScoreRed.FireOutput( pPlayer, this ); |
|
} |
|
else if( iTeam == TF_TEAM_BLUE ) |
|
{ |
|
m_onScoreBlu.FireOutput( pPlayer, this ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::OnPlayerTouchBall( CTFPlayer *pCatcher, CPasstimeBall *pBall ) |
|
{ |
|
if ( pBall != m_hBall ) |
|
{ |
|
return; |
|
} |
|
|
|
const int iCatcherTeam = pCatcher->GetTeamNumber(); |
|
float flFeet = pBall->GetAirtimeDistance() / 16.0f; |
|
auto iExperiment = (EPasstimeExperiment_Telepass) tf_passtime_experiment_telepass.GetInt(); |
|
|
|
// |
|
// Check for pass and interception |
|
// |
|
CTFPlayer *pThrower = pBall->GetThrower(); |
|
if ( pThrower // ball must must have been thrown... |
|
&& (pBall->GetCollisionCount() == 0) // and not bounced... |
|
&& (pBall->GetTeamNumber() != TEAM_UNASSIGNED) // and not be neutral... |
|
&& (pCatcher != pBall->GetPrevCarrier())) // and not passed to yourself... |
|
{ |
|
PasstimeGameEvents::PassCaught( pThrower->entindex(), pCatcher->entindex(), flFeet, pBall->GetAirtimeSec() ).Fire(); |
|
|
|
bool bAllowCheerSound = true; |
|
|
|
int iDistanceBonus = ( int ) ( pBall->GetAirtimeSec() * tf_passtime_powerball_airtimebonus.GetFloat() ); |
|
iDistanceBonus = clamp( iDistanceBonus, 0, tf_passtime_powerball_maxairtimebonus.GetInt() ); |
|
int iPassPoints = tf_passtime_powerball_passpoints.GetInt(); |
|
AddBallPower( iPassPoints + iDistanceBonus ); |
|
bAllowCheerSound = m_iBallPower < tf_passtime_powerball_threshold.GetInt(); |
|
|
|
CPASFilter pasFilter( pCatcher->GetAbsOrigin() ); |
|
pCatcher->EmitSound( pasFilter, pCatcher->entindex(), "Passtime.BallCatch" ); |
|
|
|
// make sure this happens before BeginCarry/SEtOwner etc |
|
if ( pThrower->GetTeamNumber() == iCatcherTeam ) |
|
{ |
|
if ( pBall->GetHomingTarget() ) |
|
{ |
|
// pass was caught by teammate |
|
++CTF_GameStats.m_passtimeStats.summary.nTotalPassesCompleted; |
|
CTF_GameStats.m_passtimeStats.AddPassTravelDistSample( pBall->GetAirtimeDistance() ); |
|
|
|
// award bonus effects for pass |
|
pCatcher->m_Shared.AddCond( TF_COND_SPEED_BOOST, tf_passtime_speedboost_on_get_ball_time.GetFloat() ); |
|
|
|
if ( CFuncPasstimeGoalieZone::BPlayerInAny( pCatcher ) ) |
|
{ |
|
++CTF_GameStats.m_passtimeStats.summary.nTotalPassesCompletedNearGoal; |
|
} |
|
|
|
if ( iExperiment != EPasstimeExperiment_Telepass::None ) |
|
{ |
|
// origins need to be a copy |
|
auto throwerOrigin = pThrower->GetAbsOrigin(); |
|
auto catcherOrigin = pCatcher->GetAbsOrigin(); |
|
|
|
CPVSFilter filterThrower( throwerOrigin ); |
|
switch( pThrower->GetTeamNumber() ) |
|
{ |
|
case TF_TEAM_RED: |
|
TE_TFParticleEffect( filterThrower, 0.0, "teleported_red", throwerOrigin, vec3_angle ); |
|
TE_TFParticleEffect( filterThrower, 0.0, "player_sparkles_red", throwerOrigin, vec3_angle, pThrower, PATTACH_ABSORIGIN ); |
|
break; |
|
case TF_TEAM_BLUE: |
|
TE_TFParticleEffect( filterThrower, 0.0, "teleported_blue", throwerOrigin, vec3_angle ); |
|
TE_TFParticleEffect( filterThrower, 0.0, "player_sparkles_blue", throwerOrigin, vec3_angle, pThrower, PATTACH_ABSORIGIN ); |
|
break; |
|
default: |
|
break; |
|
} |
|
|
|
pThrower->EmitSound( "Building_Teleporter.Send" ); |
|
pCatcher->EmitSound( "Building_Teleporter.Receive" ); |
|
|
|
// then move the player |
|
pThrower->Teleport( &catcherOrigin, nullptr, nullptr ); |
|
if ( iExperiment == EPasstimeExperiment_Telepass::SwapWithCatcher ) |
|
{ |
|
pCatcher->Teleport( &throwerOrigin, nullptr, nullptr ); |
|
|
|
CPVSFilter filterCatcher( catcherOrigin ); |
|
switch( pCatcher->GetTeamNumber() ) |
|
{ |
|
case TF_TEAM_RED: |
|
TE_TFParticleEffect( filterCatcher, 0.0, "teleported_red", catcherOrigin, vec3_angle ); |
|
TE_TFParticleEffect( filterCatcher, 0.0, "player_sparkles_red", catcherOrigin, vec3_angle, pCatcher, PATTACH_ABSORIGIN ); |
|
break; |
|
case TF_TEAM_BLUE: |
|
TE_TFParticleEffect( filterCatcher, 0.0, "teleported_blue", catcherOrigin, vec3_angle ); |
|
TE_TFParticleEffect( filterCatcher, 0.0, "player_sparkles_blue", catcherOrigin, vec3_angle, pCatcher, PATTACH_ABSORIGIN ); |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
|
|
// then start the effects |
|
pThrower->TeleportEffect(); |
|
} |
|
} |
|
else |
|
{ |
|
// toss was caught by teammate |
|
++CTF_GameStats.m_passtimeStats.summary.nTotalTossesCompleted; |
|
} |
|
|
|
float lastPassTime = 0.0f; |
|
for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ ) |
|
{ |
|
if ( m_ballLastPassTimes[i].first == pThrower) |
|
{ |
|
lastPassTime = m_ballLastPassTimes[i].second; |
|
break; |
|
} |
|
} |
|
|
|
// successful pass |
|
if ( flFeet > 30 ) |
|
{ |
|
// fanfare and points if the pass was long enough (and we haven't been spamming throw/catch for points) |
|
if ( gpGlobals->realtime - lastPassTime > 6.0f ) // FIXME literal balance value |
|
{ |
|
CTF_GameStats.Event_PlayerAwardBonusPoints( pThrower, pThrower, 15 ); // FIXME literal balance value |
|
} |
|
|
|
if ( bAllowCheerSound && ( pBall->GetAirtimeSec() > 2.0f ) ) // FIXME literal balance value |
|
{ |
|
TFGameRules()->BroadcastSound( 255, "TFPlayer.StunImpactRange" ); |
|
} |
|
} |
|
else// flFeet <= 30 |
|
{ |
|
// (points conditional on we haven't had the ball in the last 6 seconds) |
|
if ( gpGlobals->realtime - lastPassTime > 6.0f ) // FIXME literal balance value |
|
{ |
|
CTF_GameStats.Event_PlayerAwardBonusPoints( pThrower, pThrower, 5 ); // FIXME literal balance value |
|
} |
|
} |
|
|
|
std::pair<CTFPlayer*, float> toAdd( pThrower, gpGlobals->realtime ); |
|
bool skipTheRest = false; |
|
for ( int i = 0; i < m_ballLastPassTimes.Count(); i++ ) |
|
{ |
|
if ( m_ballLastPassTimes[i].first == pThrower) |
|
{ |
|
m_ballLastPassTimes[i].second = toAdd.second;// replace old time rather than add a new pair to the vector |
|
skipTheRest = true; |
|
break; |
|
} |
|
} |
|
|
|
if ( !skipTheRest ) |
|
{ |
|
m_ballLastPassTimes.AddToTail( toAdd ); |
|
} |
|
} |
|
else |
|
{ |
|
if ( pBall->GetHomingTarget() ) |
|
{ |
|
// pass was intercepted |
|
++CTF_GameStats.m_passtimeStats.summary.nTotalPassesIntercepted; |
|
CTF_GameStats.m_passtimeStats.AddPassTravelDistSample( pBall->GetAirtimeDistance() ); |
|
} |
|
else |
|
{ |
|
// toss was intercepted |
|
++CTF_GameStats.m_passtimeStats.summary.nTotalTossesIntercepted; |
|
} |
|
|
|
// interception can happen at any range, extra points if intercepted within the goal area |
|
int bonusPointsToAward = 15; // FIXME literal balance value |
|
if ( CFuncPasstimeGoalieZone::BPlayerInAny( pCatcher ) ) |
|
{ |
|
bonusPointsToAward = 25; // FIXME literal balance value |
|
if ( pBall->GetHomingTarget() ) |
|
{ |
|
++CTF_GameStats.m_passtimeStats.summary.nTotalPassesInterceptedNearGoal; |
|
} |
|
else |
|
{ |
|
++CTF_GameStats.m_passtimeStats.summary.nTotalTossesInterceptedNearGoal; |
|
} |
|
} |
|
|
|
// award bonus effects for interception |
|
pCatcher->m_Shared.AddCond( TF_COND_PASSTIME_INTERCEPTION, tf_passtime_speedboost_on_get_ball_time.GetFloat() ); |
|
pCatcher->m_Shared.AddCond( TF_COND_SPEED_BOOST, tf_passtime_speedboost_on_get_ball_time.GetFloat() ); |
|
|
|
CTF_GameStats.Event_PlayerAwardBonusPoints( pCatcher, pCatcher, bonusPointsToAward ); |
|
TFGameRules()->BroadcastSound( 255, "Passtime.BallIntercepted" ); |
|
CrowdReactionSound( pCatcher->GetTeamNumber() ); |
|
} |
|
} |
|
else |
|
{ |
|
++CTF_GameStats.m_passtimeStats.summary.nTotalRecoveries; |
|
CTFPlayer *pPrevCarrier = pBall->GetPrevCarrier(); |
|
if ( pCatcher != pPrevCarrier ) |
|
{ |
|
// Gain a point for picking up a neutral ball. |
|
CTF_GameStats.Event_PlayerAwardBonusPoints( pCatcher, pThrower, 5 ); // FIXME literal balance value |
|
} |
|
|
|
PasstimeGameEvents::BallGet( pCatcher->entindex() ).Fire(); |
|
} |
|
|
|
if ( ((iExperiment == EPasstimeExperiment_Telepass::TeleportToCatcherMaintainPossession) |
|
|| (iExperiment == EPasstimeExperiment_Telepass::SwapWithCatcher)) |
|
&& BCanPlayerPickUpBall( pThrower, nullptr ) ) |
|
{ |
|
EjectBall( pCatcher, pThrower ); |
|
m_hBall->SetStateCarried( pThrower ); |
|
OnBallGet(); |
|
} |
|
else |
|
{ |
|
pBall->SetStateCarried( pCatcher ); |
|
OnBallGet(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::OnBallGet() |
|
{ |
|
StopAskForBallEffects(); |
|
if ( CTFPlayer *pPlayer = m_hBall->GetCarrier() ) |
|
{ |
|
m_onBallGetAny.FireOutput( pPlayer, this ); |
|
if ( pPlayer->GetTeamNumber() == TF_TEAM_RED ) |
|
{ |
|
m_onBallGetRed.FireOutput( pPlayer, this ); |
|
} |
|
else if ( pPlayer->GetTeamNumber() == TF_TEAM_BLUE ) |
|
{ |
|
m_onBallGetBlu.FireOutput( pPlayer, this ); |
|
} |
|
CPasstimeBallController::BallPickedUp( m_hBall, pPlayer ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::InputSpawnBall( inputdata_t &input ) |
|
{ |
|
RespawnBall(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::InputTimeUp( inputdata_t &input ) |
|
{ |
|
int iRedScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_RED ); |
|
int iBlueScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_BLUE ); |
|
int iPointDifference = abs( iRedScore - iBlueScore ); |
|
|
|
// going through the list of goals to calculate the max possible point gain |
|
// is possible but tricky since goals can be enabled/disabled and there's no |
|
// way to know which goals are actually possible to score in, so this is |
|
// simply hard-coded to work correctly for the official maps where there's |
|
// a 3-point unlockable goal. |
|
int iMaxPossibleScoreGain = 3; |
|
|
|
if ( ( iPointDifference <= iMaxPossibleScoreGain ) && !ShouldEndOvertime() ) |
|
{ |
|
m_pRespawnCountdown->Disable(); |
|
TFGameRules()->BroadcastSound( 255, "Game.Overtime" ); |
|
ThinkExpiredTimer(); |
|
} |
|
else |
|
{ |
|
EndRoundExpiredTimer(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::ThinkExpiredTimer() |
|
{ |
|
if ( TFGameRules() && (TFGameRules()->State_Get() != GR_STATE_RND_RUNNING) ) |
|
{ |
|
if ( m_pRespawnCountdown ) |
|
{ |
|
// just in case |
|
m_pRespawnCountdown->Disable(); |
|
} |
|
return; |
|
} |
|
|
|
if ( ShouldEndOvertime() || m_pRespawnCountdown->Tick( gpGlobals->frametime ) ) |
|
{ |
|
EndRoundExpiredTimer(); |
|
return; |
|
} |
|
|
|
// Check again every frame until either something else ends the round |
|
// or the conditions are met that allow an expired timer to end the round. |
|
SetContextThink( &CTFPasstimeLogic::ThinkExpiredTimer, gpGlobals->curtime, "ThinkExpiredTimer" ); |
|
|
|
Assert( m_hBall ); // verified in ShouldEndOvertime |
|
Assert( m_pRespawnCountdown ); // always valid after Spawn |
|
bool bBallUnassigned = m_hBall->GetTeamNumber() == TEAM_UNASSIGNED; |
|
bool bCountdownRunning = !m_pRespawnCountdown->IsDisabled(); |
|
if ( bBallUnassigned && !bCountdownRunning ) |
|
{ |
|
// start the countdown when the ball turns neutral |
|
m_pRespawnCountdown->Start( tf_passtime_overtime_idle_sec.GetFloat() ); |
|
} |
|
else if ( !bBallUnassigned && bCountdownRunning ) |
|
{ |
|
// stop the countdown when the ball is picked up |
|
m_pRespawnCountdown->Disable(); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
bool CTFPasstimeLogic::ShouldEndOvertime() const |
|
{ |
|
if ( !m_hBall || !TFGameRules() ) |
|
{ |
|
return true; |
|
} |
|
|
|
// if nobody has the ball, only the respawn countdown can end overtime |
|
CTFPlayer *pBallCarrier = m_hBall->GetCarrier(); |
|
if ( m_hBall->GetTeamNumber() == TEAM_UNASSIGNED || !pBallCarrier ) |
|
{ |
|
return false; |
|
} |
|
|
|
// if the teams are tied, someone has to score |
|
int iRedScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_RED ); |
|
int iBluScore = TFTeamMgr()->GetFlagCaptures( TF_TEAM_BLUE ); |
|
if ( iRedScore == iBluScore ) |
|
{ |
|
return false; |
|
} |
|
|
|
// if the winning team has posession, they win |
|
int iWinningTeam = ( iRedScore > iBluScore ) |
|
? TF_TEAM_RED |
|
: TF_TEAM_BLUE; |
|
return pBallCarrier->GetTeamNumber() == iWinningTeam; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::EndRoundExpiredTimer() |
|
{ |
|
StopAskForBallEffects(); |
|
m_pRespawnCountdown->Disable(); |
|
SetContextThink( &CTFPasstimeLogic::ThinkExpiredTimer, TICK_NEVER_THINK, "ThinkExpiredTimer" ); |
|
|
|
// copied from TeamplayRoundBasedGameRules::State_Think_RND_RUNNING |
|
int iDrawScoreCheck = -1; |
|
int iWinningTeam = 0; |
|
bool bTeamsAreDrawn = true; |
|
for ( int i = FIRST_GAME_TEAM; (i < GetNumberOfTeams()) && bTeamsAreDrawn; i++ ) |
|
{ |
|
int iTeamScore = TFTeamMgr()->GetFlagCaptures( i ); |
|
|
|
if ( iTeamScore > iDrawScoreCheck ) |
|
{ |
|
iWinningTeam = i; |
|
} |
|
|
|
if ( iTeamScore != iDrawScoreCheck ) |
|
{ |
|
if ( iDrawScoreCheck == -1 ) |
|
{ |
|
iDrawScoreCheck = iTeamScore; |
|
} |
|
else |
|
{ |
|
bTeamsAreDrawn = false; |
|
} |
|
} |
|
} |
|
|
|
if ( bTeamsAreDrawn ) |
|
{ |
|
TFGameRules()->SetStalemate( STALEMATE_SERVER_TIMELIMIT, true ); |
|
} |
|
else |
|
{ |
|
TFGameRules()->SetWinningTeam( iWinningTeam, WINREASON_TIMELIMIT, true, false, false ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
struct SetSectionParams |
|
{ |
|
int num; |
|
CPathTrack *pSectionStart; |
|
CPathTrack *pSectionEnd; |
|
SetSectionParams() : num(-1), pSectionStart(0), pSectionEnd(0) {} |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
bool CTFPasstimeLogic::ParseSetSection( const char *pStr, SetSectionParams &s ) const |
|
{ |
|
char pszStartName[64]; |
|
char pszEndName[64]; |
|
const int iScanCount = sscanf( pStr, "%i %s %s", &s.num, pszStartName, pszEndName ); // WHAT YEAR IS IT |
|
if ( iScanCount != 3 ) |
|
{ |
|
return false; |
|
} |
|
s.pSectionStart = dynamic_cast<CPathTrack*>( gEntList.FindEntityByName( 0, pszStartName ) ); |
|
s.pSectionEnd = dynamic_cast<CPathTrack*>( gEntList.FindEntityByName( 0, pszEndName ) ); |
|
|
|
if ( s.num < 0 ) |
|
Warning( "SetSection number (%i) must be > 0\n", s.num ); |
|
if ( s.num >= m_iNumSections ) |
|
Warning( "SetSection number (%i) must be < section count (%i)\n", s.num, m_iNumSections.Get() ); |
|
if ( !s.pSectionStart ) |
|
Warning( "Failed to find section start path_track named %s\n", pszStartName ); |
|
if ( !s.pSectionEnd) |
|
Warning( "Failed to find section end path_track named %s\n", pszEndName ); |
|
|
|
return (s.num >= 0) |
|
&& (s.num < m_iNumSections) |
|
&& s.pSectionStart |
|
&& s.pSectionEnd; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::InputSetSection( inputdata_t &input ) |
|
{ |
|
SetSectionParams params; |
|
if ( !ParseSetSection( input.value.String(), params ) ) |
|
{ |
|
Warning( "Error in SetSection input: %s\n", input.value.String() ); |
|
return; |
|
} |
|
|
|
for ( int i = 0; i < m_trackPoints.Count(); ++i ) |
|
{ |
|
m_trackPoints.GetForModify(i).Zero(); |
|
} |
|
|
|
int iTrackPoint = 0; |
|
for ( CPathTrack *pTrack = params.pSectionStart; pTrack; pTrack = pTrack->GetNext(), ++iTrackPoint ) |
|
{ |
|
if ( iTrackPoint == m_trackPoints.Count() ) |
|
{ |
|
Warning( "Too many track_path in section (%i max, easily changed but must be fixed).", m_trackPoints.Count() ); |
|
return; |
|
} |
|
|
|
m_trackPoints.Set( iTrackPoint, pTrack->GetAbsOrigin() ); |
|
if ( pTrack->GetAbsOrigin() == Vector( 0, 0, 0 ) ) |
|
{ |
|
// Because I'm using 0,0,0 to represent "no point" in a fixed 16-element array |
|
Warning( "Can't have track_path at 0,0,0" ); |
|
} |
|
|
|
if ( pTrack == params.pSectionEnd ) |
|
{ |
|
break; |
|
} |
|
} |
|
|
|
m_iCurrentSection = params.num; |
|
|
|
} |
|
|
|
|
|
// |
|
// Secret Room |
|
// |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::SecretRoom_Spawn() |
|
{ |
|
SECRETROOM_LOG( "@@@@ SECRET ROOM: Spawn\n" ); |
|
m_SecretRoom_pTv = gEntList.FindEntityByName( nullptr, "tv" ); |
|
|
|
string_t self = GetEntityName(); |
|
|
|
// plug_breakable.OnDamaged -> this.InputPlugDamaged |
|
HookOutput( "plug_breakable", self, "OnDamaged", "staticc", nullptr, 1 ); |
|
|
|
// player triggers |
|
// the names are generated gibberish words |
|
// (Blu) (1)Scout: "comillow" |
|
HookOutput( "comillow", self, "OnStartTouch", "statica" ); |
|
HookOutput( "comillow", self, "OnEndTouch", "staticb" ); |
|
|
|
// (Red) (2)Soldier: "unissubs" |
|
HookOutput( "unissubs", self, "OnStartTouch", "statica" ); |
|
HookOutput( "unissubs", self, "OnEndTouch", "staticb" ); |
|
|
|
// (Red) (3)Pyro: "amment" |
|
HookOutput( "amment", self, "OnStartTouch", "statica" ); |
|
HookOutput( "amment", self, "OnEndTouch", "staticb" ); |
|
|
|
// (Blu) (4)Demo: "memagold" |
|
HookOutput( "memagold", self, "OnStartTouch", "statica" ); |
|
HookOutput( "memagold", self, "OnEndTouch", "staticb" ); |
|
|
|
// (Red) (5)Heavy: "subcla" |
|
HookOutput( "subcla", self, "OnStartTouch", "statica" ); |
|
HookOutput( "subcla", self, "OnEndTouch", "staticb" ); |
|
|
|
// (Blu) (6)Engineer: "enempose" |
|
HookOutput( "enempose", self, "OnStartTouch", "statica" ); |
|
HookOutput( "enempose", self, "OnEndTouch", "staticb" ); |
|
|
|
// (Red) (7)Medic: "irlenous" |
|
HookOutput( "irlenous", self, "OnStartTouch", "statica" ); |
|
HookOutput( "irlenous", self, "OnEndTouch", "staticb" ); |
|
|
|
// (Red) (8)Sniper: "donked" |
|
HookOutput( "donked", self, "OnStartTouch", "statica" ); |
|
HookOutput( "donked", self, "OnEndTouch", "staticb" ); |
|
|
|
// (Blu) (9)Spy: "finear" |
|
HookOutput( "finear", self, "OnStartTouch", "statica" ); |
|
HookOutput( "finear", self, "OnEndTouch", "staticb" ); |
|
|
|
// the room trigger for keeping track of who gets the achievement |
|
HookOutput( "room_trigger", self, "OnStartTouch", "RoomTriggerOnTouch" ); |
|
g_EventQueue.AddEvent( "room_trigger", "Enable", variant_t(), 0.0f, this, this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
int CTFPasstimeLogic::SecretRoom_CountSlottedPlayers() const |
|
{ |
|
int iNumSlotsFilled = 0; |
|
for ( CTFPlayer *pPlayer : m_SecretRoom_slottedPlayers ) |
|
{ |
|
if ( pPlayer ) ++iNumSlotsFilled; |
|
} |
|
return iNumSlotsFilled; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// this doesn't need a template, but something like this in variant_t.h would |
|
// be nice. Or maybe just some explicit overloaded constructors. |
|
template <typename T> variant_t make_variant( T value ); |
|
template <> variant_t make_variant( int value ) |
|
{ |
|
variant_t v; |
|
v.SetInt( value ); |
|
return v; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
static void SecretRoom_PlayTvSound( CSoundPatch **ppPatch, int iEntIndex, const char *pSoundName, float flVolume ) |
|
{ |
|
Assert( ppPatch ); |
|
Assert( iEntIndex > 0 ); |
|
Assert( pSoundName && *pSoundName ); |
|
Assert( flVolume > 0 ); |
|
|
|
CSoundEnvelopeController &snd = CSoundEnvelopeController::GetController(); |
|
if ( *ppPatch ) |
|
{ |
|
SECRETROOM_LOG( " @@ SECRET ROOM: Destroy sound patch\n" ); |
|
snd.SoundDestroy( *ppPatch ); |
|
*ppPatch = nullptr; |
|
} |
|
|
|
SECRETROOM_LOG( " @@ SECRET ROOM: Create sound patch for %s volume %f\n", pSoundName, flVolume ); |
|
CReliableBroadcastRecipientFilter filter; |
|
*ppPatch = snd.SoundCreate( filter, iEntIndex, pSoundName ); |
|
snd.Play( *ppPatch, flVolume, PITCH_NORM ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::SecretRoom_UpdateTv( int iNumSlotsFilled ) |
|
{ |
|
if ( iNumSlotsFilled == 9 ) |
|
{ |
|
SECRETROOM_LOG( " @@ SECRET ROOM: Update TV all slots filled\n" ); |
|
g_EventQueue.AddEvent( "screen", "Skin", make_variant( 3 ), 0.0f, this, this ); |
|
SecretRoom_PlayTvSound( &m_SecretRoom_pTvSound, |
|
m_SecretRoom_pTv->entindex(), "Passtime.Tv3", 1.0f ); |
|
} |
|
else |
|
{ |
|
// sound |
|
float volume = (float)( iNumSlotsFilled + 1 ) / 10.0f; |
|
const char *pSoundName = ( iNumSlotsFilled >= 4 ) |
|
? "Passtime.Tv2" |
|
: "Passtime.Tv1"; |
|
|
|
SECRETROOM_LOG( " @@ SECRET ROOM: Update TV %i slots filled\n", iNumSlotsFilled ); |
|
|
|
SecretRoom_PlayTvSound( &m_SecretRoom_pTvSound, |
|
m_SecretRoom_pTv->entindex(), pSoundName, volume ); |
|
|
|
// skin |
|
int iSkin = ( iNumSlotsFilled >= 4 ) ? 2 : 1; |
|
g_EventQueue.AddEvent( "screen", "Skin", make_variant( iSkin ), 0.0f, this, this ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
struct SecretRoom_TriggerInfo |
|
{ |
|
int iIndex; |
|
const char *pTriggerName; |
|
int iClass; |
|
int iTeam; |
|
} static const s_SecretRoom_TriggerInfo[9] = |
|
{ |
|
{ 0, "comillow", TF_CLASS_SCOUT, TF_TEAM_BLUE }, |
|
{ 1, "unissubs", TF_CLASS_SOLDIER, TF_TEAM_RED }, |
|
{ 2, "amment", TF_CLASS_PYRO, TF_TEAM_RED }, |
|
{ 3, "memagold", TF_CLASS_DEMOMAN, TF_TEAM_BLUE }, |
|
{ 4, "subcla", TF_CLASS_HEAVYWEAPONS, TF_TEAM_RED }, |
|
{ 5, "enempose", TF_CLASS_ENGINEER, TF_TEAM_BLUE }, |
|
{ 6, "irlenous", TF_CLASS_MEDIC, TF_TEAM_RED }, |
|
{ 7, "donked", TF_CLASS_SNIPER, TF_TEAM_RED }, |
|
{ 8, "finear", TF_CLASS_SPY, TF_TEAM_BLUE }, |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
static const SecretRoom_TriggerInfo &SecretRoom_GetSlotInfoForTrigger( |
|
const char *pTriggerName ) |
|
{ |
|
for ( const auto &info : s_SecretRoom_TriggerInfo ) |
|
{ |
|
if ( !V_strcmp( info.pTriggerName, pTriggerName ) ) |
|
{ |
|
return info; |
|
} |
|
} |
|
|
|
Error( "Invalid trigger" ); |
|
|
|
// in case some platforms don't have noreturn attribute on Error |
|
static SecretRoom_TriggerInfo unused; |
|
return unused; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// SecretRoom_InputStartTouchPlayerSlot |
|
void CTFPasstimeLogic::statica( inputdata_t &input ) |
|
{ |
|
SECRETROOM_LOG( "@@@@ SECRET ROOM: Start touch player slot\n" ); |
|
|
|
if ( m_SecretRoom_state != SecretRoomState::Open ) |
|
{ |
|
SECRETROOM_LOG( " @ SECRET ROOM: Ignore - state is not open\n" ); |
|
|
|
// shouldn't happen because triggers should be disabled |
|
return; |
|
} |
|
|
|
if ( !input.pCaller || !input.pActivator ) |
|
{ |
|
SECRETROOM_LOG( " @ SECRET ROOM: Ignore - no caller or activator\n" ); |
|
|
|
return; |
|
} |
|
|
|
CTFPlayer *pActivator = ToTFPlayer( input.pActivator ); |
|
SECRETROOM_LOG( " @ SECRET ROOM: Toucher is %s\n", pActivator->GetPlayerName() ); |
|
|
|
if ( !pActivator || pActivator->IsDead() || !pActivator->IsAlive() ) |
|
{ |
|
SECRETROOM_LOG( " @ SECRET ROOM: Ignore - bad player\n" ); |
|
|
|
// not a player or not normal |
|
return; |
|
} |
|
|
|
const char *pTriggerName = input.pCaller->GetEntityName().ToCStr(); |
|
const auto& info = SecretRoom_GetSlotInfoForTrigger( pTriggerName ); |
|
|
|
SECRETROOM_LOG( " @ SECRET ROOM: Trigger is %s, slot is %i\n", pTriggerName, info.iIndex ); |
|
|
|
|
|
if ( m_SecretRoom_slottedPlayers[info.iIndex] ) |
|
{ |
|
SECRETROOM_LOG( " @ SECRET ROOM: Ignore - slot already filled by %s\n", m_SecretRoom_slottedPlayers[info.iIndex]->GetPlayerName() ); |
|
|
|
// already someone filling the slot |
|
return; |
|
} |
|
|
|
int iActivatorTeam = pActivator->GetTeamNumber(); |
|
int iActivatorClass = pActivator->GetPlayerClass()->GetClassIndex(); |
|
|
|
if ( !pActivator |
|
|| ( info.iTeam != iActivatorTeam ) |
|
|| ( info.iClass != iActivatorClass ) ) |
|
{ |
|
SECRETROOM_LOG( " @ SECRET ROOM: Ignore - wrong class %i (%i) or team %i (%i) \n", |
|
iActivatorTeam, info.iTeam, info.iClass, iActivatorClass ); |
|
|
|
// doesn't match |
|
return; |
|
} |
|
|
|
SECRETROOM_LOG( " @ SECRET ROOM: Set slot %i to %s\n", info.iIndex, pActivator->GetPlayerName() ); |
|
|
|
// set slot |
|
m_SecretRoom_slottedPlayers[info.iIndex] = pActivator; |
|
|
|
// either solve puzzle or update effects |
|
int iNumSlotsFilled = SecretRoom_CountSlottedPlayers(); |
|
SECRETROOM_LOG( " @ SECRET ROOM: %i slots filled\n", iNumSlotsFilled ); |
|
|
|
if ( iNumSlotsFilled == 9 ) |
|
{ |
|
SecretRoom_Solve(); |
|
} |
|
else |
|
{ |
|
SecretRoom_UpdateTv( iNumSlotsFilled ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// SecretRoom_InputEndTouchPlayerSlot |
|
void CTFPasstimeLogic::staticb( inputdata_t &input ) |
|
{ |
|
SECRETROOM_LOG( "@@@@ SECRET ROOM: End touch player slot\n" ); |
|
|
|
if ( m_SecretRoom_state != SecretRoomState::Open ) |
|
{ |
|
SECRETROOM_LOG( " @ SECRET ROOM: Ignore - state is not open\n" ); |
|
|
|
// shouldn't happen because triggers should be disabled |
|
return; |
|
} |
|
|
|
const char *pTriggerName = input.pCaller->GetEntityName().ToCStr(); |
|
const auto& info = SecretRoom_GetSlotInfoForTrigger( pTriggerName ); |
|
|
|
SECRETROOM_LOG( " @ SECRET ROOM: Trigger is %s, slot is %i\n", pTriggerName, info.iIndex ); |
|
|
|
|
|
// input.pActivator can be null if a player disconnects while inside |
|
// the trigger. but there's no way to tell if it's the player occupying |
|
// the slot, so clear the slot just in case |
|
if ( input.pActivator ) |
|
{ |
|
CTFPlayer *pActivator = ToTFPlayer( input.pActivator ); |
|
SECRETROOM_LOG( " @ SECRET ROOM: Toucher is %s\n", pActivator->GetPlayerName() ); |
|
|
|
if ( !pActivator || pActivator->IsDead() || !pActivator->IsAlive() ) |
|
{ |
|
SECRETROOM_LOG( " @ SECRET ROOM: Ignore - bad player\n" ); |
|
|
|
// not a player or not normal |
|
return; |
|
} |
|
|
|
if ( m_SecretRoom_slottedPlayers[info.iIndex] != input.pActivator ) |
|
{ |
|
if ( m_SecretRoom_slottedPlayers[info.iIndex] ) |
|
SECRETROOM_LOG( " @ SECRET ROOM: Ignore - slot is held by %s\n", m_SecretRoom_slottedPlayers[info.iIndex]->GetPlayerName() ); |
|
else |
|
SECRETROOM_LOG( " @ SECRET ROOM: Ignore - slot is empty\n" ); |
|
|
|
// slot is empty already or some other player exiting the trigger |
|
|
|
// if slot is empty: due to this code not using proper filters, |
|
// this can be caused by players suiciding after changing teams |
|
// while standing inside the trigger, because the suicide happens |
|
// after the team change. this case is the entire reason for |
|
// m_SecretRoom_slottedPlayers. |
|
return; |
|
} |
|
} |
|
|
|
// clear the slot |
|
// note: in the case where two matching players are in the trigger |
|
// and the one that entered first exits, the remaining player won't count |
|
// and will have to re-enter the trigger |
|
SECRETROOM_LOG( " @ SECRET ROOM: Clear slot %i\n", info.iIndex ); |
|
|
|
m_SecretRoom_slottedPlayers[info.iIndex] = nullptr; |
|
|
|
// update effects |
|
SecretRoom_UpdateTv( SecretRoom_CountSlottedPlayers() ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// SecretRoom_InputPlugDamaged |
|
void CTFPasstimeLogic::staticc( inputdata_t &input ) |
|
{ |
|
SECRETROOM_LOG( "@@@@ SECRET ROOM: Plug destroyed\n" ); |
|
|
|
NOTE_UNUSED( input ); |
|
m_SecretRoom_state = SecretRoomState::Open; |
|
|
|
// set fx for puzzle open |
|
SecretRoom_UpdateTv( 0 ); |
|
|
|
// enable triggers |
|
for ( const auto& info : s_SecretRoom_TriggerInfo ) |
|
{ |
|
g_EventQueue.AddEvent( info.pTriggerName, "Enable", |
|
variant_t(), 0.0f, this, this ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::SecretRoom_Solve() |
|
{ |
|
if ( m_SecretRoom_state != SecretRoomState::Open ) |
|
{ |
|
// paranoia |
|
Assert( m_SecretRoom_state == SecretRoomState::Open ); |
|
return; |
|
} |
|
|
|
SECRETROOM_LOG( "@@@@ SECRET ROOM: Solved\n" ); |
|
|
|
m_SecretRoom_state = SecretRoomState::Solved; |
|
|
|
// set fx for puzzle solved |
|
g_EventQueue.AddEvent( "light", "TurnOn", variant_t(), 0.0f, this, this ); |
|
g_EventQueue.AddEvent( "spotlight", "LightOn", variant_t(), 0.0f, this, this ); |
|
g_EventQueue.AddEvent( "tv_particles", "Start", variant_t(), 0.0f, this, this ); |
|
g_EventQueue.AddEvent( "screen_image", "Enable", variant_t(), 0.0f, this, this ); |
|
SecretRoom_UpdateTv( 9 ); |
|
|
|
// disable triggers |
|
for ( const auto& info : s_SecretRoom_TriggerInfo ) |
|
{ |
|
g_EventQueue.AddEvent( info.pTriggerName, "Disable", |
|
variant_t(), 0.0f, this, this ); |
|
} |
|
|
|
// achieves |
|
for ( auto id : m_SecretRoom_playersThatTouchedRoom ) |
|
{ |
|
CTFPlayer *pPlayer = ToTFPlayer( GetPlayerBySteamID( id ) ); |
|
if ( pPlayer ) |
|
{ |
|
pPlayer->AwardAchievement( ACHIEVEMENT_TF_PASS_TIME_HAT ); |
|
} |
|
} |
|
m_SecretRoom_playersThatTouchedRoom.RemoveAll(); // paranoia |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPasstimeLogic::InputRoomTriggerOnTouch( inputdata_t &input ) |
|
{ |
|
CTFPlayer *pPlayer = ToTFPlayer( input.pActivator ); |
|
if ( !pPlayer || pPlayer->IsBot() ) |
|
{ |
|
return; |
|
} |
|
|
|
CSteamID id; |
|
pPlayer->GetSteamID( &id ); |
|
if ( id.IsValid() && ( m_SecretRoom_playersThatTouchedRoom.Find( id ) == -1 ) ) |
|
{ |
|
SECRETROOM_LOG( "@@@@ SECRET ROOM: Tracking %s for achievement\n", pPlayer->GetPlayerName() ); |
|
m_SecretRoom_playersThatTouchedRoom.AddToTail( id ); |
|
} |
|
}
|
|
|