mirror of
https://github.com/nillerusr/source-engine.git
synced 2025-04-08 09:24:44 +00:00
6172 lines
175 KiB
C++
6172 lines
175 KiB
C++
//========= Copyright © 1996-2001, Valve LLC, All rights reserved. ============
|
|
//
|
|
// Purpose: Player for HL1.
|
|
//
|
|
// $NoKeywords: $
|
|
//=============================================================================
|
|
|
|
#include "cbase.h"
|
|
#include "tf_player.h"
|
|
#include "tf_gamerules.h"
|
|
#include "tf_gamestats.h"
|
|
#include "KeyValues.h"
|
|
#include "viewport_panel_names.h"
|
|
#include "client.h"
|
|
#include "team.h"
|
|
#include "tf_weaponbase.h"
|
|
#include "tf_client.h"
|
|
#include "tf_team.h"
|
|
#include "tf_viewmodel.h"
|
|
#include "tf_item.h"
|
|
#include "in_buttons.h"
|
|
#include "entity_capture_flag.h"
|
|
#include "effect_dispatch_data.h"
|
|
#include "te_effect_dispatch.h"
|
|
#include "game.h"
|
|
#include "tf_weapon_builder.h"
|
|
#include "tf_obj.h"
|
|
#include "tf_ammo_pack.h"
|
|
#include "datacache/imdlcache.h"
|
|
#include "particle_parse.h"
|
|
#include "props_shared.h"
|
|
#include "filesystem.h"
|
|
#include "toolframework_server.h"
|
|
#include "IEffects.h"
|
|
#include "func_respawnroom.h"
|
|
#include "networkstringtable_gamedll.h"
|
|
#include "team_control_point_master.h"
|
|
#include "tf_weapon_pda.h"
|
|
#include "sceneentity.h"
|
|
#include "fmtstr.h"
|
|
#include "tf_weapon_sniperrifle.h"
|
|
#include "tf_weapon_minigun.h"
|
|
#include "trigger_area_capture.h"
|
|
#include "triggers.h"
|
|
#include "tf_weapon_medigun.h"
|
|
#include "hl2orange.spa.h"
|
|
#include "te_tfblood.h"
|
|
#include "activitylist.h"
|
|
#include "steam/steam_api.h"
|
|
#include "cdll_int.h"
|
|
#include "tf_weaponbase.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
#define DAMAGE_FORCE_SCALE_SELF 9
|
|
|
|
extern bool IsInCommentaryMode( void );
|
|
|
|
extern ConVar sk_player_head;
|
|
extern ConVar sk_player_chest;
|
|
extern ConVar sk_player_stomach;
|
|
extern ConVar sk_player_arm;
|
|
extern ConVar sk_player_leg;
|
|
|
|
extern ConVar tf_spy_invis_time;
|
|
extern ConVar tf_spy_invis_unstealth_time;
|
|
extern ConVar tf_stalematechangeclasstime;
|
|
|
|
EHANDLE g_pLastSpawnPoints[TF_TEAM_COUNT];
|
|
|
|
ConVar tf_playerstatetransitions( "tf_playerstatetransitions", "-2", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "tf_playerstatetransitions <ent index or -1 for all>. Show player state transitions." );
|
|
ConVar tf_playergib( "tf_playergib", "1", FCVAR_PROTECTED, "Allow player gibbing." );
|
|
|
|
ConVar tf_weapon_ragdoll_velocity_min( "tf_weapon_ragdoll_velocity_min", "100", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
|
|
ConVar tf_weapon_ragdoll_velocity_max( "tf_weapon_ragdoll_velocity_max", "150", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
|
|
ConVar tf_weapon_ragdoll_maxspeed( "tf_weapon_ragdoll_maxspeed", "300", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
|
|
|
|
ConVar tf_damageforcescale_other( "tf_damageforcescale_other", "6.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
|
|
ConVar tf_damageforcescale_self_soldier( "tf_damageforcescale_self_soldier", "10.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
|
|
ConVar tf_damagescale_self_soldier( "tf_damagescale_self_soldier", "0.60", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
|
|
|
|
ConVar tf_damage_lineardist( "tf_damage_lineardist", "0", FCVAR_DEVELOPMENTONLY );
|
|
ConVar tf_damage_range( "tf_damage_range", "0.5", FCVAR_DEVELOPMENTONLY );
|
|
|
|
ConVar tf_max_voice_speak_delay( "tf_max_voice_speak_delay", "1.5", FCVAR_DEVELOPMENTONLY, "Max time after a voice command until player can do another one" );
|
|
|
|
extern ConVar spec_freeze_time;
|
|
extern ConVar spec_freeze_traveltime;
|
|
extern ConVar sv_maxunlag;
|
|
extern ConVar tf_damage_disablespread;
|
|
extern ConVar tf_gravetalk;
|
|
extern ConVar tf_spectalk;
|
|
|
|
// -------------------------------------------------------------------------------- //
|
|
// Player animation event. Sent to the client when a player fires, jumps, reloads, etc..
|
|
// -------------------------------------------------------------------------------- //
|
|
|
|
class CTEPlayerAnimEvent : public CBaseTempEntity
|
|
{
|
|
public:
|
|
DECLARE_CLASS( CTEPlayerAnimEvent, CBaseTempEntity );
|
|
DECLARE_SERVERCLASS();
|
|
|
|
CTEPlayerAnimEvent( const char *name ) : CBaseTempEntity( name )
|
|
{
|
|
m_iPlayerIndex = TF_PLAYER_INDEX_NONE;
|
|
}
|
|
|
|
CNetworkVar( int, m_iPlayerIndex );
|
|
CNetworkVar( int, m_iEvent );
|
|
CNetworkVar( int, m_nData );
|
|
};
|
|
|
|
IMPLEMENT_SERVERCLASS_ST_NOBASE( CTEPlayerAnimEvent, DT_TEPlayerAnimEvent )
|
|
SendPropInt( SENDINFO( m_iPlayerIndex ), 7, SPROP_UNSIGNED ),
|
|
SendPropInt( SENDINFO( m_iEvent ), Q_log2( PLAYERANIMEVENT_COUNT ) + 1, SPROP_UNSIGNED ),
|
|
// BUGBUG: ywb we assume this is either 0 or an animation sequence #, but it could also be an activity, which should fit within this limit, but we're not guaranteed.
|
|
SendPropInt( SENDINFO( m_nData ), ANIMATION_SEQUENCE_BITS ),
|
|
END_SEND_TABLE()
|
|
|
|
static CTEPlayerAnimEvent g_TEPlayerAnimEvent( "PlayerAnimEvent" );
|
|
|
|
void TE_PlayerAnimEvent( CBasePlayer *pPlayer, PlayerAnimEvent_t event, int nData )
|
|
{
|
|
Vector vecEyePos = pPlayer->EyePosition();
|
|
CPVSFilter filter( vecEyePos );
|
|
if ( !IsCustomPlayerAnimEvent( event ) && ( event != PLAYERANIMEVENT_SNAP_YAW ) && ( event != PLAYERANIMEVENT_VOICE_COMMAND_GESTURE ) )
|
|
{
|
|
filter.RemoveRecipient( pPlayer );
|
|
}
|
|
|
|
Assert( pPlayer->entindex() >= 1 && pPlayer->entindex() <= MAX_PLAYERS );
|
|
g_TEPlayerAnimEvent.m_iPlayerIndex = pPlayer->entindex();
|
|
g_TEPlayerAnimEvent.m_iEvent = event;
|
|
Assert( nData < (1<<ANIMATION_SEQUENCE_BITS) );
|
|
Assert( (1<<ANIMATION_SEQUENCE_BITS) >= ActivityList_HighestIndex() );
|
|
g_TEPlayerAnimEvent.m_nData = nData;
|
|
g_TEPlayerAnimEvent.Create( filter, 0 );
|
|
}
|
|
|
|
//=================================================================================
|
|
//
|
|
// Ragdoll Entity
|
|
//
|
|
class CTFRagdoll : public CBaseAnimatingOverlay
|
|
{
|
|
public:
|
|
|
|
DECLARE_CLASS( CTFRagdoll, CBaseAnimatingOverlay );
|
|
DECLARE_SERVERCLASS();
|
|
|
|
CTFRagdoll()
|
|
{
|
|
m_iPlayerIndex.Set( TF_PLAYER_INDEX_NONE );
|
|
m_bGib = false;
|
|
m_bBurning = false;
|
|
m_vecRagdollOrigin.Init();
|
|
m_vecRagdollVelocity.Init();
|
|
}
|
|
|
|
// Transmit ragdolls to everyone.
|
|
virtual int UpdateTransmitState()
|
|
{
|
|
return SetTransmitState( FL_EDICT_ALWAYS );
|
|
}
|
|
|
|
CNetworkVar( int, m_iPlayerIndex );
|
|
CNetworkVector( m_vecRagdollVelocity );
|
|
CNetworkVector( m_vecRagdollOrigin );
|
|
CNetworkVar( bool, m_bGib );
|
|
CNetworkVar( bool, m_bBurning );
|
|
CNetworkVar( int, m_iTeam );
|
|
CNetworkVar( int, m_iClass );
|
|
};
|
|
|
|
LINK_ENTITY_TO_CLASS( tf_ragdoll, CTFRagdoll );
|
|
|
|
IMPLEMENT_SERVERCLASS_ST_NOBASE( CTFRagdoll, DT_TFRagdoll )
|
|
SendPropVector( SENDINFO( m_vecRagdollOrigin ), -1, SPROP_COORD ),
|
|
SendPropInt( SENDINFO( m_iPlayerIndex ), 7, SPROP_UNSIGNED ),
|
|
SendPropVector ( SENDINFO(m_vecForce), -1, SPROP_NOSCALE ),
|
|
SendPropVector( SENDINFO( m_vecRagdollVelocity ), 13, SPROP_ROUNDDOWN, -2048.0f, 2048.0f ),
|
|
SendPropInt( SENDINFO( m_nForceBone ) ),
|
|
SendPropBool( SENDINFO( m_bGib ) ),
|
|
SendPropBool( SENDINFO( m_bBurning ) ),
|
|
SendPropInt( SENDINFO( m_iTeam ), 3, SPROP_UNSIGNED ),
|
|
SendPropInt( SENDINFO( m_iClass ), 4, SPROP_UNSIGNED ),
|
|
END_SEND_TABLE()
|
|
|
|
// -------------------------------------------------------------------------------- //
|
|
// Tables.
|
|
// -------------------------------------------------------------------------------- //
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Filters updates to a variable so that only non-local players see
|
|
// the changes. This is so we can send a low-res origin to non-local players
|
|
// while sending a hi-res one to the local player.
|
|
// Input : *pVarData -
|
|
// *pOut -
|
|
// objectID -
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void* SendProxy_SendNonLocalDataTable( const SendProp *pProp, const void *pStruct, const void *pVarData, CSendProxyRecipients *pRecipients, int objectID )
|
|
{
|
|
pRecipients->SetAllRecipients();
|
|
pRecipients->ClearRecipient( objectID - 1 );
|
|
return ( void * )pVarData;
|
|
}
|
|
REGISTER_SEND_PROXY_NON_MODIFIED_POINTER( SendProxy_SendNonLocalDataTable );
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: SendProxy that converts the UtlVector list of objects to entindexes, where it's reassembled on the client
|
|
//-----------------------------------------------------------------------------
|
|
void SendProxy_PlayerObjectList( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID )
|
|
{
|
|
CTFPlayer *pPlayer = (CTFPlayer*)pStruct;
|
|
|
|
// If this fails, then SendProxyArrayLength_PlayerObjects didn't work.
|
|
Assert( iElement < pPlayer->GetObjectCount() );
|
|
|
|
CBaseObject *pObject = pPlayer->GetObject(iElement);
|
|
|
|
EHANDLE hObject;
|
|
hObject = pObject;
|
|
|
|
SendProxy_EHandleToInt( pProp, pStruct, &hObject, pOut, iElement, objectID );
|
|
}
|
|
|
|
int SendProxyArrayLength_PlayerObjects( const void *pStruct, int objectID )
|
|
{
|
|
CTFPlayer *pPlayer = (CTFPlayer*)pStruct;
|
|
int iObjects = pPlayer->GetObjectCount();
|
|
Assert( iObjects <= MAX_OBJECTS_PER_PLAYER );
|
|
return iObjects;
|
|
}
|
|
|
|
BEGIN_DATADESC( CTFPlayer )
|
|
END_DATADESC()
|
|
extern void SendProxy_Origin( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID );
|
|
|
|
// specific to the local player
|
|
BEGIN_SEND_TABLE_NOBASE( CTFPlayer, DT_TFLocalPlayerExclusive )
|
|
// send a hi-res origin to the local player for use in prediction
|
|
SendPropVector (SENDINFO(m_vecOrigin), -1, SPROP_NOSCALE|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_Origin ),
|
|
SendPropArray2(
|
|
SendProxyArrayLength_PlayerObjects,
|
|
SendPropInt("player_object_array_element", 0, SIZEOF_IGNORE, NUM_NETWORKED_EHANDLE_BITS, SPROP_UNSIGNED, SendProxy_PlayerObjectList),
|
|
MAX_OBJECTS_PER_PLAYER,
|
|
0,
|
|
"player_object_array"
|
|
),
|
|
|
|
SendPropFloat( SENDINFO_VECTORELEM(m_angEyeAngles, 0), 8, SPROP_CHANGES_OFTEN, -90.0f, 90.0f ),
|
|
// SendPropAngle( SENDINFO_VECTORELEM(m_angEyeAngles, 1), 10, SPROP_CHANGES_OFTEN ),
|
|
|
|
END_SEND_TABLE()
|
|
|
|
// all players except the local player
|
|
BEGIN_SEND_TABLE_NOBASE( CTFPlayer, DT_TFNonLocalPlayerExclusive )
|
|
// send a lo-res origin to other players
|
|
SendPropVector (SENDINFO(m_vecOrigin), -1, SPROP_COORD_MP_LOWPRECISION|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_Origin ),
|
|
|
|
SendPropFloat( SENDINFO_VECTORELEM(m_angEyeAngles, 0), 8, SPROP_CHANGES_OFTEN, -90.0f, 90.0f ),
|
|
SendPropAngle( SENDINFO_VECTORELEM(m_angEyeAngles, 1), 10, SPROP_CHANGES_OFTEN ),
|
|
|
|
END_SEND_TABLE()
|
|
|
|
|
|
//============
|
|
|
|
LINK_ENTITY_TO_CLASS( player, CTFPlayer );
|
|
PRECACHE_REGISTER(player);
|
|
|
|
IMPLEMENT_SERVERCLASS_ST( CTFPlayer, DT_TFPlayer )
|
|
SendPropExclude( "DT_BaseAnimating", "m_flPoseParameter" ),
|
|
SendPropExclude( "DT_BaseAnimating", "m_flPlaybackRate" ),
|
|
SendPropExclude( "DT_BaseAnimating", "m_nSequence" ),
|
|
SendPropExclude( "DT_BaseEntity", "m_angRotation" ),
|
|
SendPropExclude( "DT_BaseAnimatingOverlay", "overlay_vars" ),
|
|
SendPropExclude( "DT_BaseEntity", "m_nModelIndex" ),
|
|
SendPropExclude( "DT_BaseEntity", "m_vecOrigin" ),
|
|
|
|
// cs_playeranimstate and clientside animation takes care of these on the client
|
|
SendPropExclude( "DT_ServerAnimationData" , "m_flCycle" ),
|
|
SendPropExclude( "DT_AnimTimeMustBeFirst" , "m_flAnimTime" ),
|
|
|
|
SendPropExclude( "DT_BaseFlex", "m_flexWeight" ),
|
|
SendPropExclude( "DT_BaseFlex", "m_blinktoggle" ),
|
|
SendPropExclude( "DT_BaseFlex", "m_viewtarget" ),
|
|
|
|
SendPropBool(SENDINFO(m_bSaveMeParity)),
|
|
|
|
// This will create a race condition will the local player, but the data will be the same so.....
|
|
SendPropInt( SENDINFO( m_nWaterLevel ), 2, SPROP_UNSIGNED ),
|
|
|
|
SendPropEHandle(SENDINFO(m_hItem)),
|
|
|
|
// Ragdoll.
|
|
SendPropEHandle( SENDINFO( m_hRagdoll ) ),
|
|
|
|
SendPropDataTable( SENDINFO_DT( m_PlayerClass ), &REFERENCE_SEND_TABLE( DT_TFPlayerClassShared ) ),
|
|
SendPropDataTable( SENDINFO_DT( m_Shared ), &REFERENCE_SEND_TABLE( DT_TFPlayerShared ) ),
|
|
|
|
// Data that only gets sent to the local player
|
|
SendPropDataTable( "tflocaldata", 0, &REFERENCE_SEND_TABLE(DT_TFLocalPlayerExclusive), SendProxy_SendLocalDataTable ),
|
|
|
|
// Data that gets sent to all other players
|
|
SendPropDataTable( "tfnonlocaldata", 0, &REFERENCE_SEND_TABLE(DT_TFNonLocalPlayerExclusive), SendProxy_SendNonLocalDataTable ),
|
|
|
|
SendPropBool( SENDINFO( m_iSpawnCounter ) ),
|
|
|
|
END_SEND_TABLE()
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------------- //
|
|
|
|
void cc_CreatePredictionError_f()
|
|
{
|
|
CBaseEntity *pEnt = CBaseEntity::Instance( 1 );
|
|
pEnt->SetAbsOrigin( pEnt->GetAbsOrigin() + Vector( 63, 0, 0 ) );
|
|
}
|
|
|
|
ConCommand cc_CreatePredictionError( "CreatePredictionError", cc_CreatePredictionError_f, "Create a prediction error", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
|
|
|
|
// Hint callbacks
|
|
bool HintCallbackNeedsResources_Sentrygun( CBasePlayer *pPlayer )
|
|
{
|
|
return ( pPlayer->GetAmmoCount( TF_AMMO_METAL ) > CalculateObjectCost( OBJ_SENTRYGUN ) );
|
|
}
|
|
bool HintCallbackNeedsResources_Dispenser( CBasePlayer *pPlayer )
|
|
{
|
|
return ( pPlayer->GetAmmoCount( TF_AMMO_METAL ) > CalculateObjectCost( OBJ_DISPENSER ) );
|
|
}
|
|
bool HintCallbackNeedsResources_Teleporter( CBasePlayer *pPlayer )
|
|
{
|
|
return ( pPlayer->GetAmmoCount( TF_AMMO_METAL ) > CalculateObjectCost( OBJ_TELEPORTER_ENTRANCE ) );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
CTFPlayer::CTFPlayer()
|
|
{
|
|
m_PlayerAnimState = CreateTFPlayerAnimState( this );
|
|
item_list = 0;
|
|
|
|
SetArmorValue( 10 );
|
|
|
|
m_hItem = NULL;
|
|
m_hTauntScene = NULL;
|
|
|
|
UseClientSideAnimation();
|
|
m_angEyeAngles.Init();
|
|
m_pStateInfo = NULL;
|
|
m_lifeState = LIFE_DEAD; // Start "dead".
|
|
m_iMaxSentryKills = 0;
|
|
m_flNextNameChangeTime = 0;
|
|
|
|
m_flNextTimeCheck = gpGlobals->curtime;
|
|
m_flSpawnTime = 0;
|
|
|
|
SetViewOffset( TF_PLAYER_VIEW_OFFSET );
|
|
|
|
m_Shared.Init( this );
|
|
|
|
m_iLastSkin = -1;
|
|
|
|
m_bHudClassAutoKill = false;
|
|
m_bMedigunAutoHeal = false;
|
|
|
|
m_vecLastDeathPosition = Vector( FLT_MAX, FLT_MAX, FLT_MAX );
|
|
|
|
SetDesiredPlayerClassIndex( TF_CLASS_UNDEFINED );
|
|
|
|
SetContextThink( &CTFPlayer::TFPlayerThink, gpGlobals->curtime, "TFPlayerThink" );
|
|
|
|
ResetScores();
|
|
|
|
m_flLastAction = gpGlobals->curtime;
|
|
|
|
m_bInitTaunt = false;
|
|
|
|
m_bSpeakingConceptAsDisguisedSpy = false;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::TFPlayerThink()
|
|
{
|
|
if ( m_pStateInfo && m_pStateInfo->pfnThink )
|
|
{
|
|
(this->*m_pStateInfo->pfnThink)();
|
|
}
|
|
|
|
// Time to finish the current random expression? Or time to pick a new one?
|
|
if ( IsAlive() && m_flNextRandomExpressionTime >= 0 && gpGlobals->curtime > m_flNextRandomExpressionTime )
|
|
{
|
|
// Random expressions need to be cleared, because they don't loop. So if we
|
|
// pick the same one again, we want to restart it.
|
|
ClearExpression();
|
|
m_iszExpressionScene = NULL_STRING;
|
|
UpdateExpression();
|
|
}
|
|
|
|
// Check to see if we are in the air and taunting. Stop if so.
|
|
if ( GetGroundEntity() == NULL && m_Shared.InCond( TF_COND_TAUNTING ) )
|
|
{
|
|
if( m_hTauntScene.Get() )
|
|
{
|
|
StopScriptedScene( this, m_hTauntScene );
|
|
m_Shared.m_flTauntRemoveTime = 0.0f;
|
|
m_hTauntScene = NULL;
|
|
}
|
|
}
|
|
|
|
SetContextThink( &CTFPlayer::TFPlayerThink, gpGlobals->curtime, "TFPlayerThink" );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::MedicRegenThink( void )
|
|
{
|
|
if ( IsPlayerClass( TF_CLASS_MEDIC ) )
|
|
{
|
|
if ( IsAlive() )
|
|
{
|
|
// Heal faster if we haven't been in combat for a while
|
|
float flTimeSinceDamage = gpGlobals->curtime - GetLastDamageTime();
|
|
float flScale = RemapValClamped( flTimeSinceDamage, 5, 10, 1.0, 3.0 );
|
|
|
|
int iHealAmount = ceil(TF_MEDIC_REGEN_AMOUNT * flScale);
|
|
TakeHealth( iHealAmount, DMG_GENERIC );
|
|
}
|
|
|
|
SetContextThink( &CTFPlayer::MedicRegenThink, gpGlobals->curtime + TF_MEDIC_REGEN_TIME, "MedicRegenThink" );
|
|
}
|
|
}
|
|
|
|
CTFPlayer::~CTFPlayer()
|
|
{
|
|
DestroyRagdoll();
|
|
m_PlayerAnimState->Release();
|
|
}
|
|
|
|
|
|
CTFPlayer *CTFPlayer::CreatePlayer( const char *className, edict_t *ed )
|
|
{
|
|
CTFPlayer::s_PlayerEdict = ed;
|
|
return (CTFPlayer*)CreateEntityByName( className );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::UpdateTimers( void )
|
|
{
|
|
m_Shared.ConditionThink();
|
|
m_Shared.InvisibilityThink();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::PreThink()
|
|
{
|
|
// Update timers.
|
|
UpdateTimers();
|
|
|
|
// Pass through to the base class think.
|
|
BaseClass::PreThink();
|
|
|
|
// Reset bullet force accumulator, only lasts one frame, for ragdoll forces from multiple shots.
|
|
m_vecTotalBulletForce = vec3_origin;
|
|
|
|
CheckForIdle();
|
|
}
|
|
|
|
ConVar mp_idledealmethod( "mp_idledealmethod", "1", FCVAR_GAMEDLL, "Deals with Idle Players. 1 = Sends them into Spectator mode then kicks them if they're still idle, 2 = Kicks them out of the game;" );
|
|
ConVar mp_idlemaxtime( "mp_idlemaxtime", "3", FCVAR_GAMEDLL, "Maximum time a player is allowed to be idle (in minutes)" );
|
|
|
|
void CTFPlayer::CheckForIdle( void )
|
|
{
|
|
if ( m_afButtonLast != m_nButtons )
|
|
m_flLastAction = gpGlobals->curtime;
|
|
|
|
if ( mp_idledealmethod.GetInt() )
|
|
{
|
|
if ( IsHLTV() )
|
|
return;
|
|
|
|
if ( IsFakeClient() )
|
|
return;
|
|
|
|
//Don't mess with the host on a listen server (probably one of us debugging something)
|
|
if ( engine->IsDedicatedServer() == false && entindex() == 1 )
|
|
return;
|
|
|
|
if ( m_bIsIdle == false )
|
|
{
|
|
if ( StateGet() == TF_STATE_OBSERVER || StateGet() != TF_STATE_ACTIVE )
|
|
return;
|
|
}
|
|
|
|
float flIdleTime = mp_idlemaxtime.GetFloat() * 60;
|
|
|
|
if ( TFGameRules()->InStalemate() )
|
|
{
|
|
flIdleTime = mp_stalemate_timelimit.GetInt() * 0.5f;
|
|
}
|
|
|
|
if ( (gpGlobals->curtime - m_flLastAction) > flIdleTime )
|
|
{
|
|
bool bKickPlayer = false;
|
|
|
|
ConVarRef mp_allowspectators( "mp_allowspectators" );
|
|
if ( mp_allowspectators.IsValid() && ( mp_allowspectators.GetBool() == false ) )
|
|
{
|
|
// just kick the player if this server doesn't allow spectators
|
|
bKickPlayer = true;
|
|
}
|
|
else if ( mp_idledealmethod.GetInt() == 1 )
|
|
{
|
|
//First send them into spectator mode then kick him.
|
|
if ( m_bIsIdle == false )
|
|
{
|
|
ForceChangeTeam( TEAM_SPECTATOR );
|
|
m_flLastAction = gpGlobals->curtime;
|
|
m_bIsIdle = true;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
bKickPlayer = true;
|
|
}
|
|
}
|
|
else if ( mp_idledealmethod.GetInt() == 2 )
|
|
{
|
|
bKickPlayer = true;
|
|
}
|
|
|
|
if ( bKickPlayer == true )
|
|
{
|
|
UTIL_ClientPrintAll( HUD_PRINTCONSOLE, "#game_idle_kick", GetPlayerName() );
|
|
engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", GetUserID() ) );
|
|
m_flLastAction = gpGlobals->curtime;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extern ConVar flashlight;
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPlayer::FlashlightIsOn( void )
|
|
{
|
|
return IsEffectActive( EF_DIMLIGHT );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::FlashlightTurnOn( void )
|
|
{
|
|
if( flashlight.GetInt() > 0 && IsAlive() )
|
|
{
|
|
AddEffects( EF_DIMLIGHT );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::FlashlightTurnOff( void )
|
|
{
|
|
if( IsEffectActive(EF_DIMLIGHT) )
|
|
{
|
|
RemoveEffects( EF_DIMLIGHT );
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::PostThink()
|
|
{
|
|
BaseClass::PostThink();
|
|
|
|
QAngle angles = GetLocalAngles();
|
|
angles[PITCH] = 0;
|
|
SetLocalAngles( angles );
|
|
|
|
// Store the eye angles pitch so the client can compute its animation state correctly.
|
|
m_angEyeAngles = EyeAngles();
|
|
|
|
m_PlayerAnimState->Update( m_angEyeAngles[YAW], m_angEyeAngles[PITCH] );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::Precache()
|
|
{
|
|
// Precache the player models and gibs.
|
|
PrecachePlayerModels();
|
|
|
|
// Precache the player sounds.
|
|
PrecacheScriptSound( "Player.Spawn" );
|
|
PrecacheScriptSound( "TFPlayer.Pain" );
|
|
PrecacheScriptSound( "TFPlayer.CritHit" );
|
|
PrecacheScriptSound( "TFPlayer.CritPain" );
|
|
PrecacheScriptSound( "TFPlayer.CritDeath" );
|
|
PrecacheScriptSound( "TFPlayer.FreezeCam" );
|
|
PrecacheScriptSound( "TFPlayer.Drown" );
|
|
PrecacheScriptSound( "TFPlayer.AttackerPain" );
|
|
PrecacheScriptSound( "TFPlayer.SaveMe" );
|
|
PrecacheScriptSound( "Camera.SnapShot" );
|
|
|
|
PrecacheScriptSound( "Game.YourTeamLost" );
|
|
PrecacheScriptSound( "Game.YourTeamWon" );
|
|
PrecacheScriptSound( "Game.SuddenDeath" );
|
|
PrecacheScriptSound( "Game.Stalemate" );
|
|
PrecacheScriptSound( "TV.Tune" );
|
|
|
|
// Precache particle systems
|
|
PrecacheParticleSystem( "crit_text" );
|
|
PrecacheParticleSystem( "cig_smoke" );
|
|
PrecacheParticleSystem( "speech_mediccall" );
|
|
PrecacheParticleSystem( "player_recent_teleport_blue" );
|
|
PrecacheParticleSystem( "player_recent_teleport_red" );
|
|
PrecacheParticleSystem( "particle_nemesis_red" );
|
|
PrecacheParticleSystem( "particle_nemesis_blue" );
|
|
PrecacheParticleSystem( "spy_start_disguise_red" );
|
|
PrecacheParticleSystem( "spy_start_disguise_blue" );
|
|
PrecacheParticleSystem( "burningplayer_red" );
|
|
PrecacheParticleSystem( "burningplayer_blue" );
|
|
PrecacheParticleSystem( "blood_spray_red_01" );
|
|
PrecacheParticleSystem( "blood_spray_red_01_far" );
|
|
PrecacheParticleSystem( "water_blood_impact_red_01" );
|
|
PrecacheParticleSystem( "blood_impact_red_01" );
|
|
PrecacheParticleSystem( "water_playerdive" );
|
|
PrecacheParticleSystem( "water_playeremerge" );
|
|
|
|
BaseClass::Precache();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Precache the player models and player model gibs.
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::PrecachePlayerModels( void )
|
|
{
|
|
int i;
|
|
for ( i = 0; i < TF_CLASS_COUNT_ALL; i++ )
|
|
{
|
|
const char *pszModel = GetPlayerClassData( i )->m_szModelName;
|
|
if ( pszModel && pszModel[0] )
|
|
{
|
|
int iModel = PrecacheModel( pszModel );
|
|
PrecacheGibsForModel( iModel );
|
|
}
|
|
|
|
if ( !IsX360() )
|
|
{
|
|
// Precache the hardware facial morphed models as well.
|
|
const char *pszHWMModel = GetPlayerClassData( i )->m_szHWMModelName;
|
|
if ( pszHWMModel && pszHWMModel[0] )
|
|
{
|
|
PrecacheModel( pszHWMModel );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( TFGameRules() && TFGameRules()->IsBirthday() )
|
|
{
|
|
for ( i = 1; i < ARRAYSIZE(g_pszBDayGibs); i++ )
|
|
{
|
|
PrecacheModel( g_pszBDayGibs[i] );
|
|
}
|
|
PrecacheModel( "models/effects/bday_hat.mdl" );
|
|
}
|
|
|
|
// Precache player class sounds
|
|
for ( i = TF_FIRST_NORMAL_CLASS; i < TF_CLASS_COUNT_ALL; ++i )
|
|
{
|
|
TFPlayerClassData_t *pData = GetPlayerClassData( i );
|
|
|
|
PrecacheScriptSound( pData->m_szDeathSound );
|
|
PrecacheScriptSound( pData->m_szCritDeathSound );
|
|
PrecacheScriptSound( pData->m_szMeleeDeathSound );
|
|
PrecacheScriptSound( pData->m_szExplosionDeathSound );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::IsReadyToPlay( void )
|
|
{
|
|
return ( ( GetTeamNumber() > LAST_SHARED_TEAM ) &&
|
|
( GetDesiredPlayerClassIndex() > TF_CLASS_UNDEFINED ) );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::IsReadyToSpawn( void )
|
|
{
|
|
if ( IsClassMenuOpen() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return ( StateGet() != TF_STATE_DYING );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Return true if this player should be allowed to instantly spawn
|
|
// when they next finish picking a class.
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::ShouldGainInstantSpawn( void )
|
|
{
|
|
return ( GetPlayerClass()->GetClassIndex() == TF_CLASS_UNDEFINED || IsClassMenuOpen() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Resets player scores
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::ResetScores( void )
|
|
{
|
|
CTF_GameStats.ResetPlayerStats( this );
|
|
RemoveNemesisRelationships();
|
|
BaseClass::ResetScores();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::InitialSpawn( void )
|
|
{
|
|
BaseClass::InitialSpawn();
|
|
|
|
SetWeaponBuilder( NULL );
|
|
|
|
m_iMaxSentryKills = 0;
|
|
CTF_GameStats.Event_MaxSentryKills( this, 0 );
|
|
|
|
StateEnter( TF_STATE_WELCOME );
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::Spawn()
|
|
{
|
|
MDLCACHE_CRITICAL_SECTION();
|
|
|
|
m_flSpawnTime = gpGlobals->curtime;
|
|
UpdateModel();
|
|
|
|
SetMoveType( MOVETYPE_WALK );
|
|
BaseClass::Spawn();
|
|
|
|
// Create our off hand viewmodel if necessary
|
|
CreateViewModel( 1 );
|
|
// Make sure it has no model set, in case it had one before
|
|
GetViewModel(1)->SetModel( "" );
|
|
|
|
// Kind of lame, but CBasePlayer::Spawn resets a lot of the state that we initially want on.
|
|
// So if we're in the welcome state, call its enter function to reset
|
|
if ( m_Shared.InState( TF_STATE_WELCOME ) )
|
|
{
|
|
StateEnterWELCOME();
|
|
}
|
|
|
|
// If they were dead, then they're respawning. Put them in the active state.
|
|
if ( m_Shared.InState( TF_STATE_DYING ) )
|
|
{
|
|
StateTransition( TF_STATE_ACTIVE );
|
|
}
|
|
|
|
// If they're spawning into the world as fresh meat, give them items and stuff.
|
|
if ( m_Shared.InState( TF_STATE_ACTIVE ) )
|
|
{
|
|
// remove our disguise each time we spawn
|
|
if ( m_Shared.InCond( TF_COND_DISGUISED ) )
|
|
{
|
|
m_Shared.RemoveDisguise();
|
|
}
|
|
|
|
EmitSound( "Player.Spawn" );
|
|
InitClass();
|
|
m_Shared.RemoveAllCond( NULL ); // Remove conc'd, burning, rotting, hallucinating, etc.
|
|
|
|
UpdateSkin( GetTeamNumber() );
|
|
TeamFortress_SetSpeed();
|
|
|
|
// Prevent firing for a second so players don't blow their faces off
|
|
SetNextAttack( gpGlobals->curtime + 1.0 );
|
|
|
|
DoAnimationEvent( PLAYERANIMEVENT_SPAWN );
|
|
|
|
// Force a taunt off, if we are still taunting, the condition should have been cleared above.
|
|
if( m_hTauntScene.Get() )
|
|
{
|
|
StopScriptedScene( this, m_hTauntScene );
|
|
m_Shared.m_flTauntRemoveTime = 0.0f;
|
|
m_hTauntScene = NULL;
|
|
}
|
|
|
|
// turn on separation so players don't get stuck in each other when spawned
|
|
m_Shared.SetSeparation( true );
|
|
m_Shared.SetSeparationVelocity( vec3_origin );
|
|
|
|
RemoveTeleportEffect();
|
|
|
|
//If this is true it means I respawned without dying (changing class inside the spawn room) but doesn't necessarily mean that my healers have stopped healing me
|
|
//This means that medics can still be linked to me but my health would not be affected since this condition is not set.
|
|
//So instead of going and forcing every healer on me to stop healing we just set this condition back on.
|
|
//If the game decides I shouldn't be healed by someone (LOS, Distance, etc) they will break the link themselves like usual.
|
|
if ( m_Shared.GetNumHealers() > 0 )
|
|
{
|
|
m_Shared.AddCond( TF_COND_HEALTH_BUFF );
|
|
}
|
|
|
|
if ( !m_bSeenRoundInfo )
|
|
{
|
|
TFGameRules()->ShowRoundInfoPanel( this );
|
|
m_bSeenRoundInfo = true;
|
|
}
|
|
|
|
if ( IsInCommentaryMode() && !IsFakeClient() )
|
|
{
|
|
// Player is spawning in commentary mode. Tell the commentary system.
|
|
CBaseEntity *pEnt = NULL;
|
|
variant_t emptyVariant;
|
|
while ( (pEnt = gEntList.FindEntityByClassname( pEnt, "commentary_auto" )) != NULL )
|
|
{
|
|
pEnt->AcceptInput( "MultiplayerSpawned", this, this, emptyVariant, 0 );
|
|
}
|
|
}
|
|
}
|
|
|
|
CTF_GameStats.Event_PlayerSpawned( this );
|
|
|
|
m_iSpawnCounter = !m_iSpawnCounter;
|
|
m_bAllowInstantSpawn = false;
|
|
|
|
m_Shared.SetSpyCloakMeter( 100.0f );
|
|
|
|
m_Shared.ClearDamageEvents();
|
|
ClearDamagerHistory();
|
|
|
|
m_flLastDamageTime = 0;
|
|
|
|
m_flNextVoiceCommandTime = gpGlobals->curtime;
|
|
|
|
ClearZoomOwner();
|
|
SetFOV( this , 0 );
|
|
|
|
SetViewOffset( GetClassEyeHeight() );
|
|
|
|
ClearExpression();
|
|
m_flNextSpeakWeaponFire = gpGlobals->curtime;
|
|
|
|
m_bIsIdle = false;
|
|
m_flPowerPlayTime = 0.0;
|
|
|
|
// This makes the surrounding box always the same size as the standing collision box
|
|
// helps with parts of the hitboxes that extend out of the crouching hitbox, eg with the
|
|
// heavyweapons guy
|
|
Vector mins = VEC_HULL_MIN;
|
|
Vector maxs = VEC_HULL_MAX;
|
|
CollisionProp()->SetSurroundingBoundsType( USE_SPECIFIED_BOUNDS, &mins, &maxs );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Removes all nemesis relationships between this player and others
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::RemoveNemesisRelationships()
|
|
{
|
|
for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ )
|
|
{
|
|
CTFPlayer *pTemp = ToTFPlayer( UTIL_PlayerByIndex( i ) );
|
|
if ( pTemp && pTemp != this )
|
|
{
|
|
// set this player to be not dominating anyone else
|
|
m_Shared.SetPlayerDominated( pTemp, false );
|
|
|
|
// set no one else to be dominating this player
|
|
pTemp->m_Shared.SetPlayerDominated( this, false );
|
|
}
|
|
}
|
|
// reset the matrix of who has killed whom with respect to this player
|
|
CTF_GameStats.ResetKillHistory( this );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::Regenerate( void )
|
|
{
|
|
// We may have been boosted over our max health. If we have,
|
|
// restore it after we reset out class values.
|
|
int iCurrentHealth = GetHealth();
|
|
m_bRegenerating = true;
|
|
InitClass();
|
|
m_bRegenerating = false;
|
|
if ( iCurrentHealth > GetHealth() )
|
|
{
|
|
SetHealth( iCurrentHealth );
|
|
}
|
|
|
|
if ( m_Shared.InCond( TF_COND_BURNING ) )
|
|
{
|
|
m_Shared.RemoveCond( TF_COND_BURNING );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::InitClass( void )
|
|
{
|
|
// Set initial health and armor based on class.
|
|
SetMaxHealth( GetPlayerClass()->GetMaxHealth() );
|
|
SetHealth( GetMaxHealth() );
|
|
|
|
SetArmorValue( GetPlayerClass()->GetMaxArmor() );
|
|
|
|
// Init the anim movement vars
|
|
m_PlayerAnimState->SetRunSpeed( GetPlayerClass()->GetMaxSpeed() );
|
|
m_PlayerAnimState->SetWalkSpeed( GetPlayerClass()->GetMaxSpeed() * 0.5 );
|
|
|
|
// Give default items for class.
|
|
GiveDefaultItems();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::CreateViewModel( int iViewModel )
|
|
{
|
|
Assert( iViewModel >= 0 && iViewModel < MAX_VIEWMODELS );
|
|
|
|
if ( GetViewModel( iViewModel ) )
|
|
return;
|
|
|
|
CTFViewModel *pViewModel = ( CTFViewModel * )CreateEntityByName( "tf_viewmodel" );
|
|
if ( pViewModel )
|
|
{
|
|
pViewModel->SetAbsOrigin( GetAbsOrigin() );
|
|
pViewModel->SetOwner( this );
|
|
pViewModel->SetIndex( iViewModel );
|
|
DispatchSpawn( pViewModel );
|
|
pViewModel->FollowEntity( this, false );
|
|
m_hViewModel.Set( iViewModel, pViewModel );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Gets the view model for the player's off hand
|
|
//-----------------------------------------------------------------------------
|
|
CBaseViewModel *CTFPlayer::GetOffHandViewModel()
|
|
{
|
|
// off hand model is slot 1
|
|
return GetViewModel( 1 );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Sends the specified animation activity to the off hand view model
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::SendOffHandViewModelActivity( Activity activity )
|
|
{
|
|
CBaseViewModel *pViewModel = GetOffHandViewModel();
|
|
if ( pViewModel )
|
|
{
|
|
int sequence = pViewModel->SelectWeightedSequence( activity );
|
|
pViewModel->SendViewModelMatchingSequence( sequence );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Set the player up with the default weapons, ammo, etc.
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::GiveDefaultItems()
|
|
{
|
|
// Get the player class data.
|
|
TFPlayerClassData_t *pData = m_PlayerClass.GetData();
|
|
|
|
RemoveAllAmmo();
|
|
|
|
// Give ammo. Must be done before weapons, so weapons know the player has ammo for them.
|
|
for ( int iAmmo = 0; iAmmo < TF_AMMO_COUNT; ++iAmmo )
|
|
{
|
|
GiveAmmo( pData->m_aAmmoMax[iAmmo], iAmmo );
|
|
}
|
|
|
|
// Give weapons.
|
|
ManageRegularWeapons( pData );
|
|
|
|
// Give a builder weapon for each object the playerclass is allowed to build
|
|
ManageBuilderWeapons( pData );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::ManageBuilderWeapons( TFPlayerClassData_t *pData )
|
|
{
|
|
if ( pData->m_aBuildable[0] != OBJ_LAST )
|
|
{
|
|
CTFWeaponBase *pBuilder = Weapon_OwnsThisID( TF_WEAPON_BUILDER );
|
|
|
|
// Give the player a new builder weapon when they switch between engy and spy
|
|
if ( pBuilder && !GetPlayerClass()->CanBuildObject( pBuilder->GetSubType() ) )
|
|
{
|
|
Weapon_Detach( pBuilder );
|
|
UTIL_Remove( pBuilder );
|
|
|
|
pBuilder = NULL;
|
|
}
|
|
|
|
if ( pBuilder )
|
|
{
|
|
pBuilder->GiveDefaultAmmo();
|
|
pBuilder->ChangeTeam( GetTeamNumber() );
|
|
|
|
if ( m_bRegenerating == false )
|
|
{
|
|
pBuilder->WeaponReset();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pBuilder = (CTFWeaponBase *)GiveNamedItem( "tf_weapon_builder" );
|
|
|
|
if ( pBuilder )
|
|
{
|
|
pBuilder->SetSubType( pData->m_aBuildable[0] );
|
|
pBuilder->DefaultTouch( this );
|
|
}
|
|
}
|
|
|
|
if ( pBuilder )
|
|
{
|
|
pBuilder->m_nSkin = GetTeamNumber() - 2; // color the w_model to the team
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//Not supposed to be holding a builder, nuke it from orbit
|
|
CTFWeaponBase *pWpn = Weapon_OwnsThisID( TF_WEAPON_BUILDER );
|
|
|
|
if ( pWpn == NULL )
|
|
return;
|
|
|
|
Weapon_Detach( pWpn );
|
|
UTIL_Remove( pWpn );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::ManageRegularWeapons( TFPlayerClassData_t *pData )
|
|
{
|
|
for ( int iWeapon = 0; iWeapon < TF_PLAYER_WEAPON_COUNT; ++iWeapon )
|
|
{
|
|
if ( pData->m_aWeapons[iWeapon] != TF_WEAPON_NONE )
|
|
{
|
|
int iWeaponID = pData->m_aWeapons[iWeapon];
|
|
const char *pszWeaponName = WeaponIdToClassname( iWeaponID );
|
|
|
|
CTFWeaponBase *pWeapon = (CTFWeaponBase *)GetWeapon( iWeapon );
|
|
|
|
//If we already have a weapon in this slot but is not the same type then nuke it (changed classes)
|
|
if ( pWeapon && pWeapon->GetWeaponID() != iWeaponID )
|
|
{
|
|
Weapon_Detach( pWeapon );
|
|
UTIL_Remove( pWeapon );
|
|
}
|
|
|
|
pWeapon = (CTFWeaponBase *)Weapon_OwnsThisID( iWeaponID );
|
|
|
|
if ( pWeapon )
|
|
{
|
|
pWeapon->ChangeTeam( GetTeamNumber() );
|
|
pWeapon->GiveDefaultAmmo();
|
|
|
|
if ( m_bRegenerating == false )
|
|
{
|
|
pWeapon->WeaponReset();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pWeapon = (CTFWeaponBase *)GiveNamedItem( pszWeaponName );
|
|
|
|
if ( pWeapon )
|
|
{
|
|
pWeapon->DefaultTouch( this );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//I shouldn't have any weapons in this slot, so get rid of it
|
|
CTFWeaponBase *pCarriedWeapon = (CTFWeaponBase *)GetWeapon( iWeapon );
|
|
|
|
//Don't nuke builders since they will be nuked if we don't need them later.
|
|
if ( pCarriedWeapon && pCarriedWeapon->GetWeaponID() != TF_WEAPON_BUILDER )
|
|
{
|
|
Weapon_Detach( pCarriedWeapon );
|
|
UTIL_Remove( pCarriedWeapon );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( m_bRegenerating == false )
|
|
{
|
|
SetActiveWeapon( NULL );
|
|
Weapon_Switch( Weapon_GetSlot( 0 ) );
|
|
Weapon_SetLast( Weapon_GetSlot( 1 ) );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Find a spawn point for the player.
|
|
//-----------------------------------------------------------------------------
|
|
CBaseEntity* CTFPlayer::EntSelectSpawnPoint()
|
|
{
|
|
CBaseEntity *pSpot = g_pLastSpawnPoints[ GetTeamNumber() ];
|
|
const char *pSpawnPointName = "";
|
|
|
|
switch( GetTeamNumber() )
|
|
{
|
|
case TF_TEAM_RED:
|
|
case TF_TEAM_BLUE:
|
|
{
|
|
pSpawnPointName = "info_player_teamspawn";
|
|
if ( SelectSpawnSpot( pSpawnPointName, pSpot ) )
|
|
{
|
|
g_pLastSpawnPoints[ GetTeamNumber() ] = pSpot;
|
|
}
|
|
|
|
// need to save this for later so we can apply and modifiers to the armor and grenades...after the call to InitClass()
|
|
m_pSpawnPoint = dynamic_cast<CTFTeamSpawn*>( pSpot );
|
|
break;
|
|
}
|
|
case TEAM_SPECTATOR:
|
|
case TEAM_UNASSIGNED:
|
|
default:
|
|
{
|
|
pSpot = CBaseEntity::Instance( INDEXENT(0) );
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !pSpot )
|
|
{
|
|
Warning( "PutClientInServer: no %s on level\n", pSpawnPointName );
|
|
return CBaseEntity::Instance( INDEXENT(0) );
|
|
}
|
|
|
|
return pSpot;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::SelectSpawnSpot( const char *pEntClassName, CBaseEntity* &pSpot )
|
|
{
|
|
// Get an initial spawn point.
|
|
pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName );
|
|
if ( !pSpot )
|
|
{
|
|
// Sometimes the first spot can be NULL????
|
|
pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName );
|
|
}
|
|
|
|
// First we try to find a spawn point that is fully clear. If that fails,
|
|
// we look for a spawnpoint that's clear except for another players. We
|
|
// don't collide with our team members, so we should be fine.
|
|
bool bIgnorePlayers = false;
|
|
|
|
CBaseEntity *pFirstSpot = pSpot;
|
|
do
|
|
{
|
|
if ( pSpot )
|
|
{
|
|
// Check to see if this is a valid team spawn (player is on this team, etc.).
|
|
if( TFGameRules()->IsSpawnPointValid( pSpot, this, bIgnorePlayers ) )
|
|
{
|
|
// Check for a bad spawn entity.
|
|
if ( pSpot->GetAbsOrigin() == Vector( 0, 0, 0 ) )
|
|
{
|
|
pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName );
|
|
continue;
|
|
}
|
|
|
|
// Found a valid spawn point.
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Get the next spawning point to check.
|
|
pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName );
|
|
|
|
if ( pSpot == pFirstSpot && !bIgnorePlayers )
|
|
{
|
|
// Loop through again, ignoring players
|
|
bIgnorePlayers = true;
|
|
pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName );
|
|
}
|
|
}
|
|
// Continue until a valid spawn point is found or we hit the start.
|
|
while ( pSpot != pFirstSpot );
|
|
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::DoAnimationEvent( PlayerAnimEvent_t event, int nData )
|
|
{
|
|
m_PlayerAnimState->DoAnimationEvent( event, nData );
|
|
TE_PlayerAnimEvent( this, event, nData ); // Send to any clients who can see this guy.
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::PhysObjectSleep()
|
|
{
|
|
IPhysicsObject *pObj = VPhysicsGetObject();
|
|
if ( pObj )
|
|
pObj->Sleep();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::PhysObjectWake()
|
|
{
|
|
IPhysicsObject *pObj = VPhysicsGetObject();
|
|
if ( pObj )
|
|
pObj->Wake();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPlayer::GetAutoTeam( void )
|
|
{
|
|
int iTeam = TEAM_SPECTATOR;
|
|
|
|
CTFTeam *pBlue = TFTeamMgr()->GetTeam( TF_TEAM_BLUE );
|
|
CTFTeam *pRed = TFTeamMgr()->GetTeam( TF_TEAM_RED );
|
|
|
|
if ( pBlue && pRed )
|
|
{
|
|
if ( pBlue->GetNumPlayers() < pRed->GetNumPlayers() )
|
|
{
|
|
iTeam = TF_TEAM_BLUE;
|
|
}
|
|
else if ( pRed->GetNumPlayers() < pBlue->GetNumPlayers() )
|
|
{
|
|
iTeam = TF_TEAM_RED;
|
|
}
|
|
else
|
|
{
|
|
iTeam = RandomInt( 0, 1 ) ? TF_TEAM_RED : TF_TEAM_BLUE;
|
|
}
|
|
}
|
|
|
|
return iTeam;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::HandleCommand_JoinTeam( const char *pTeamName )
|
|
{
|
|
int iTeam = TF_TEAM_RED;
|
|
if ( stricmp( pTeamName, "auto" ) == 0 )
|
|
{
|
|
iTeam = GetAutoTeam();
|
|
}
|
|
else if ( stricmp( pTeamName, "spectate" ) == 0 )
|
|
{
|
|
iTeam = TEAM_SPECTATOR;
|
|
}
|
|
else
|
|
{
|
|
for ( int i = 0; i < TF_TEAM_COUNT; ++i )
|
|
{
|
|
if ( stricmp( pTeamName, g_aTeamNames[i] ) == 0 )
|
|
{
|
|
iTeam = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( iTeam == TEAM_SPECTATOR )
|
|
{
|
|
// Prevent this is the cvar is set
|
|
if ( !mp_allowspectators.GetInt() && !IsHLTV() )
|
|
{
|
|
ClientPrint( this, HUD_PRINTCENTER, "#Cannot_Be_Spectator" );
|
|
return;
|
|
}
|
|
|
|
if ( GetTeamNumber() != TEAM_UNASSIGNED && !IsDead() )
|
|
{
|
|
CommitSuicide( false, true );
|
|
}
|
|
|
|
ChangeTeam( TEAM_SPECTATOR );
|
|
|
|
// do we have fadetoblack on? (need to fade their screen back in)
|
|
if ( mp_fadetoblack.GetBool() )
|
|
{
|
|
color32_s clr = { 0,0,0,255 };
|
|
UTIL_ScreenFade( this, clr, 0, 0, FFADE_IN | FFADE_PURGE );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( iTeam == GetTeamNumber() )
|
|
{
|
|
return; // we wouldn't change the team
|
|
}
|
|
|
|
// if this join would unbalance the teams, refuse
|
|
// come up with a better way to tell the player they tried to join a full team!
|
|
if ( TFGameRules()->WouldChangeUnbalanceTeams( iTeam, GetTeamNumber() ) )
|
|
{
|
|
ShowViewPortPanel( PANEL_TEAM );
|
|
return;
|
|
}
|
|
|
|
ChangeTeam( iTeam );
|
|
|
|
ShowViewPortPanel( ( iTeam == TF_TEAM_RED ) ? PANEL_CLASS_RED : PANEL_CLASS_BLUE );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Join a team without using the game menus
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::HandleCommand_JoinTeam_NoMenus( const char *pTeamName )
|
|
{
|
|
Assert( IsX360() );
|
|
|
|
Msg( "Client command HandleCommand_JoinTeam_NoMenus: %s\n", pTeamName );
|
|
|
|
// Only expected to be used on the 360 when players leave the lobby to start a new game
|
|
if ( !IsInCommentaryMode() )
|
|
{
|
|
Assert( GetTeamNumber() == TEAM_UNASSIGNED );
|
|
Assert( IsX360() );
|
|
}
|
|
|
|
int iTeam = TEAM_SPECTATOR;
|
|
if ( Q_stricmp( pTeamName, "spectate" ) )
|
|
{
|
|
for ( int i = 0; i < TF_TEAM_COUNT; ++i )
|
|
{
|
|
if ( stricmp( pTeamName, g_aTeamNames[i] ) == 0 )
|
|
{
|
|
iTeam = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
ForceChangeTeam( iTeam );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Player has been forcefully changed to another team
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::ForceChangeTeam( int iTeamNum )
|
|
{
|
|
int iNewTeam = iTeamNum;
|
|
|
|
if ( iNewTeam == TF_TEAM_AUTOASSIGN )
|
|
{
|
|
iNewTeam = GetAutoTeam();
|
|
}
|
|
|
|
if ( !GetGlobalTeam( iNewTeam ) )
|
|
{
|
|
Warning( "CTFPlayer::ForceChangeTeam( %d ) - invalid team index.\n", iNewTeam );
|
|
return;
|
|
}
|
|
|
|
int iOldTeam = GetTeamNumber();
|
|
|
|
// if this is our current team, just abort
|
|
if ( iNewTeam == iOldTeam )
|
|
return;
|
|
|
|
RemoveAllObjects();
|
|
RemoveNemesisRelationships();
|
|
|
|
BaseClass::ChangeTeam( iNewTeam );
|
|
|
|
if ( iNewTeam == TEAM_UNASSIGNED )
|
|
{
|
|
StateTransition( TF_STATE_OBSERVER );
|
|
}
|
|
else if ( iNewTeam == TEAM_SPECTATOR )
|
|
{
|
|
m_bIsIdle = false;
|
|
StateTransition( TF_STATE_OBSERVER );
|
|
|
|
RemoveAllWeapons();
|
|
DestroyViewModels();
|
|
}
|
|
|
|
// Don't modify living players in any way
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::HandleFadeToBlack( void )
|
|
{
|
|
if ( mp_fadetoblack.GetBool() )
|
|
{
|
|
color32_s clr = { 0,0,0,255 };
|
|
UTIL_ScreenFade( this, clr, 0.75, 0, FFADE_OUT | FFADE_STAYOUT );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::ChangeTeam( int iTeamNum )
|
|
{
|
|
if ( !GetGlobalTeam( iTeamNum ) )
|
|
{
|
|
Warning( "CTFPlayer::ChangeTeam( %d ) - invalid team index.\n", iTeamNum );
|
|
return;
|
|
}
|
|
|
|
int iOldTeam = GetTeamNumber();
|
|
|
|
// if this is our current team, just abort
|
|
if ( iTeamNum == iOldTeam )
|
|
return;
|
|
|
|
RemoveAllObjects();
|
|
RemoveNemesisRelationships();
|
|
|
|
BaseClass::ChangeTeam( iTeamNum );
|
|
|
|
if ( iTeamNum == TEAM_UNASSIGNED )
|
|
{
|
|
StateTransition( TF_STATE_OBSERVER );
|
|
}
|
|
else if ( iTeamNum == TEAM_SPECTATOR )
|
|
{
|
|
m_bIsIdle = false;
|
|
|
|
StateTransition( TF_STATE_OBSERVER );
|
|
|
|
RemoveAllWeapons();
|
|
DestroyViewModels();
|
|
}
|
|
else // active player
|
|
{
|
|
if ( !IsDead() && (iOldTeam == TF_TEAM_RED || iOldTeam == TF_TEAM_BLUE) )
|
|
{
|
|
// Kill player if switching teams while alive
|
|
CommitSuicide( false, true );
|
|
}
|
|
else if ( IsDead() && iOldTeam < FIRST_GAME_TEAM )
|
|
{
|
|
SetObserverMode( OBS_MODE_CHASE );
|
|
HandleFadeToBlack();
|
|
}
|
|
|
|
// let any spies disguising as me know that I've changed teams
|
|
for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ )
|
|
{
|
|
CTFPlayer *pTemp = ToTFPlayer( UTIL_PlayerByIndex( i ) );
|
|
if ( pTemp && pTemp != this )
|
|
{
|
|
if ( ( pTemp->m_Shared.GetDisguiseTarget() == this ) || // they were disguising as me and I've changed teams
|
|
( !pTemp->m_Shared.GetDisguiseTarget() && pTemp->m_Shared.GetDisguiseTeam() == iTeamNum ) ) // they don't have a disguise and I'm joining the team they're disguising as
|
|
{
|
|
// choose someone else...
|
|
pTemp->m_Shared.FindDisguiseTarget();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::HandleCommand_JoinClass( const char *pClassName )
|
|
{
|
|
// can only join a class after you join a valid team
|
|
if ( GetTeamNumber() <= LAST_SHARED_TEAM )
|
|
return;
|
|
|
|
// In case we don't get the class menu message before the spawn timer
|
|
// comes up, fake that we've closed the menu.
|
|
SetClassMenuOpen( false );
|
|
|
|
if ( TFGameRules()->InStalemate() )
|
|
{
|
|
if ( IsAlive() && !TFGameRules()->CanChangeClassInStalemate() )
|
|
{
|
|
ClientPrint(this, HUD_PRINTTALK, "#game_stalemate_cant_change_class" );
|
|
return;
|
|
}
|
|
}
|
|
|
|
int iClass = TF_CLASS_UNDEFINED;
|
|
bool bShouldNotRespawn = false;
|
|
|
|
if ( ( TFGameRules()->State_Get() == GR_STATE_TEAM_WIN ) && ( TFGameRules()->GetWinningTeam() != GetTeamNumber() ) )
|
|
{
|
|
m_bAllowInstantSpawn = false;
|
|
bShouldNotRespawn = true;
|
|
}
|
|
|
|
if ( stricmp( pClassName, "random" ) != 0 )
|
|
{
|
|
int i = 0;
|
|
|
|
for ( i = TF_CLASS_SCOUT ; i < TF_CLASS_COUNT_ALL ; i++ )
|
|
{
|
|
if ( stricmp( pClassName, GetPlayerClassData( i )->m_szClassName ) == 0 )
|
|
{
|
|
iClass = i;
|
|
break;
|
|
}
|
|
}
|
|
if ( i > TF_LAST_NORMAL_CLASS )
|
|
{
|
|
Warning( "HandleCommand_JoinClass( %s ) - invalid class name.\n", pClassName );
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The player has selected Random class...so let's pick one for them.
|
|
do{
|
|
// Don't let them be the same class twice in a row
|
|
iClass = random->RandomInt( TF_FIRST_NORMAL_CLASS, TF_LAST_NORMAL_CLASS );
|
|
} while( iClass == GetPlayerClass()->GetClassIndex() );
|
|
}
|
|
|
|
// joining the same class?
|
|
if ( iClass != TF_CLASS_RANDOM && iClass == GetDesiredPlayerClassIndex() )
|
|
{
|
|
// If we're dead, and we have instant spawn, respawn us immediately. Catches the case
|
|
// were a player misses respawn wave because they're at the class menu, and then changes
|
|
// their mind and reselects their current class.
|
|
if ( m_bAllowInstantSpawn && !IsAlive() )
|
|
{
|
|
ForceRespawn();
|
|
}
|
|
return;
|
|
}
|
|
|
|
SetDesiredPlayerClassIndex( iClass );
|
|
IGameEvent * event = gameeventmanager->CreateEvent( "player_changeclass" );
|
|
if ( event )
|
|
{
|
|
event->SetInt( "userid", GetUserID() );
|
|
event->SetInt( "class", iClass );
|
|
|
|
gameeventmanager->FireEvent( event );
|
|
}
|
|
|
|
// are they TF_CLASS_RANDOM and trying to select the class they're currently playing as (so they can stay this class)?
|
|
if ( iClass == GetPlayerClass()->GetClassIndex() )
|
|
{
|
|
// If we're dead, and we have instant spawn, respawn us immediately. Catches the case
|
|
// were a player misses respawn wave because they're at the class menu, and then changes
|
|
// their mind and reselects their current class.
|
|
if ( m_bAllowInstantSpawn && !IsAlive() )
|
|
{
|
|
ForceRespawn();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// We can respawn instantly if:
|
|
// - We're dead, and we're past the required post-death time
|
|
// - We're inside a respawn room
|
|
// - We're in the stalemate grace period
|
|
bool bInRespawnRoom = PointInRespawnRoom( this, WorldSpaceCenter() );
|
|
if ( bInRespawnRoom && !IsAlive() )
|
|
{
|
|
// If we're not spectating ourselves, ignore respawn rooms. Otherwise we'll get instant spawns
|
|
// by spectating someone inside a respawn room.
|
|
bInRespawnRoom = (GetObserverTarget() == this);
|
|
}
|
|
bool bDeadInstantSpawn = !IsAlive();
|
|
if ( bDeadInstantSpawn && m_flDeathTime )
|
|
{
|
|
// In death mode, don't allow class changes to force respawns ahead of respawn waves
|
|
float flWaveTime = TFGameRules()->GetNextRespawnWave( GetTeamNumber(), this );
|
|
bDeadInstantSpawn = (gpGlobals->curtime > flWaveTime);
|
|
}
|
|
bool bInStalemateClassChangeTime = false;
|
|
if ( TFGameRules()->InStalemate() )
|
|
{
|
|
// Stalemate overrides respawn rules. Only allow spawning if we're in the class change time.
|
|
bInStalemateClassChangeTime = TFGameRules()->CanChangeClassInStalemate();
|
|
bDeadInstantSpawn = false;
|
|
bInRespawnRoom = false;
|
|
}
|
|
if ( bShouldNotRespawn == false && ( m_bAllowInstantSpawn || bDeadInstantSpawn || bInRespawnRoom || bInStalemateClassChangeTime ) )
|
|
{
|
|
ForceRespawn();
|
|
return;
|
|
}
|
|
|
|
if( iClass == TF_CLASS_RANDOM )
|
|
{
|
|
if( IsAlive() )
|
|
{
|
|
ClientPrint(this, HUD_PRINTTALK, "#game_respawn_asrandom" );
|
|
}
|
|
else
|
|
{
|
|
ClientPrint(this, HUD_PRINTTALK, "#game_spawn_asrandom" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( IsAlive() )
|
|
{
|
|
ClientPrint(this, HUD_PRINTTALK, "#game_respawn_as", GetPlayerClassData( iClass )->m_szLocalizableName );
|
|
}
|
|
else
|
|
{
|
|
ClientPrint(this, HUD_PRINTTALK, "#game_spawn_as", GetPlayerClassData( iClass )->m_szLocalizableName );
|
|
}
|
|
}
|
|
|
|
if ( IsAlive() && ( GetHudClassAutoKill() == true ) && bShouldNotRespawn == false )
|
|
{
|
|
CommitSuicide( false, true );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::ClientCommand( const CCommand &args )
|
|
{
|
|
const char *pcmd = args[0];
|
|
|
|
m_flLastAction = gpGlobals->curtime;
|
|
|
|
#ifdef _DEBUG
|
|
if ( FStrEq( pcmd, "addcond" ) )
|
|
{
|
|
if ( args.ArgC() >= 2 )
|
|
{
|
|
int iCond = clamp( atoi( args[1] ), 0, TF_COND_LAST-1 );
|
|
|
|
CTFPlayer *pTargetPlayer = this;
|
|
if ( args.ArgC() >= 4 )
|
|
{
|
|
// Find the matching netname
|
|
for ( int i = 1; i <= gpGlobals->maxClients; i++ )
|
|
{
|
|
CBasePlayer *pPlayer = ToBasePlayer( UTIL_PlayerByIndex(i) );
|
|
if ( pPlayer )
|
|
{
|
|
if ( Q_strstr( pPlayer->GetPlayerName(), args[3] ) )
|
|
{
|
|
pTargetPlayer = ToTFPlayer(pPlayer);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( args.ArgC() >= 3 )
|
|
{
|
|
float flDuration = atof( args[2] );
|
|
pTargetPlayer->m_Shared.AddCond( iCond, flDuration );
|
|
}
|
|
else
|
|
{
|
|
pTargetPlayer->m_Shared.AddCond( iCond );
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "removecond" ) )
|
|
{
|
|
if ( args.ArgC() >= 2 )
|
|
{
|
|
int iCond = clamp( atoi( args[1] ), 0, TF_COND_LAST-1 );
|
|
m_Shared.RemoveCond( iCond );
|
|
}
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "burn" ) )
|
|
{
|
|
m_Shared.Burn( this );
|
|
return true;
|
|
}
|
|
else
|
|
#endif
|
|
|
|
if ( FStrEq( pcmd, "jointeam" ) )
|
|
{
|
|
if ( args.ArgC() >= 2 )
|
|
{
|
|
HandleCommand_JoinTeam( args[1] );
|
|
}
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "jointeam_nomenus" ) )
|
|
{
|
|
if ( IsX360() )
|
|
{
|
|
if ( args.ArgC() >= 2 )
|
|
{
|
|
HandleCommand_JoinTeam_NoMenus( args[1] );
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
else if ( FStrEq( pcmd, "closedwelcomemenu" ) )
|
|
{
|
|
if ( GetTeamNumber() == TEAM_UNASSIGNED )
|
|
{
|
|
ShowViewPortPanel( PANEL_TEAM, true );
|
|
}
|
|
else if ( IsPlayerClass( TF_CLASS_UNDEFINED ) )
|
|
{
|
|
switch( GetTeamNumber() )
|
|
{
|
|
case TF_TEAM_RED:
|
|
ShowViewPortPanel( PANEL_CLASS_RED, true );
|
|
break;
|
|
|
|
case TF_TEAM_BLUE:
|
|
ShowViewPortPanel( PANEL_CLASS_BLUE, true );
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "joinclass" ) )
|
|
{
|
|
if ( args.ArgC() >= 2 )
|
|
{
|
|
HandleCommand_JoinClass( args[1] );
|
|
}
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "disguise" ) )
|
|
{
|
|
if ( args.ArgC() >= 3 )
|
|
{
|
|
if ( CanDisguise() )
|
|
{
|
|
int nClass = atoi( args[ 1 ] );
|
|
int nTeam = atoi( args[ 2 ] );
|
|
|
|
// intercepting the team value and reassigning what gets passed into Disguise()
|
|
// because the team numbers in the client menu don't match the #define values for the teams
|
|
m_Shared.Disguise( ( nTeam == 1 ) ? TF_TEAM_BLUE : TF_TEAM_RED, nClass );
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else if (FStrEq( pcmd, "lastdisguise" ) )
|
|
{
|
|
// disguise as our last known disguise. desired disguise will be initted to something sensible
|
|
if ( CanDisguise() )
|
|
{
|
|
// disguise as the previous class, if one exists
|
|
int nClass = m_Shared.GetDesiredDisguiseClass();
|
|
|
|
//If we pass in "random" or whatever then just make it pick a random class.
|
|
if ( args.ArgC() > 1 )
|
|
{
|
|
nClass = TF_CLASS_UNDEFINED;
|
|
}
|
|
|
|
if ( nClass == TF_CLASS_UNDEFINED )
|
|
{
|
|
// they haven't disguised yet, pick a nice one for them.
|
|
// exclude some undesirable classes
|
|
do
|
|
{
|
|
nClass = random->RandomInt( TF_FIRST_NORMAL_CLASS, TF_LAST_NORMAL_CLASS );
|
|
} while( nClass == TF_CLASS_SCOUT || nClass == TF_CLASS_SPY );
|
|
}
|
|
|
|
m_Shared.Disguise( ( GetTeamNumber() == TF_TEAM_BLUE ) ? TF_TEAM_RED : TF_TEAM_BLUE, nClass );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "mp_playgesture" ) )
|
|
{
|
|
if ( args.ArgC() == 1 )
|
|
{
|
|
Warning( "mp_playgesture: Gesture activity or sequence must be specified!\n" );
|
|
return true;
|
|
}
|
|
|
|
if ( sv_cheats->GetBool() )
|
|
{
|
|
if ( !PlayGesture( args[1] ) )
|
|
{
|
|
Warning( "mp_playgesture: unknown sequence or activity name \"%s\"\n", args[1] );
|
|
return true;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "mp_playanimation" ) )
|
|
{
|
|
if ( args.ArgC() == 1 )
|
|
{
|
|
Warning( "mp_playanimation: Activity or sequence must be specified!\n" );
|
|
return true;
|
|
}
|
|
|
|
if ( sv_cheats->GetBool() )
|
|
{
|
|
if ( !PlaySpecificSequence( args[1] ) )
|
|
{
|
|
Warning( "mp_playanimation: Unknown sequence or activity name \"%s\"\n", args[1] );
|
|
return true;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "menuopen" ) )
|
|
{
|
|
SetClassMenuOpen( true );
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "menuclosed" ) )
|
|
{
|
|
SetClassMenuOpen( false );
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "pda_click" ) )
|
|
{
|
|
// player clicked on the PDA, play attack animation
|
|
|
|
CTFWeaponBase *pWpn = GetActiveTFWeapon();
|
|
|
|
CTFWeaponPDA *pPDA = dynamic_cast<CTFWeaponPDA *>( pWpn );
|
|
|
|
if ( pPDA && !m_Shared.InCond( TF_COND_DISGUISED ) )
|
|
{
|
|
DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRIMARY );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "taunt" ) )
|
|
{
|
|
Taunt();
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "build" ) )
|
|
{
|
|
if ( args.ArgC() == 2 )
|
|
{
|
|
// player wants to build something
|
|
int iBuilding = atoi( args[ 1 ] );
|
|
|
|
StartBuildingObjectOfType( iBuilding );
|
|
}
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "destroy" ) )
|
|
{
|
|
if ( args.ArgC() == 2 )
|
|
{
|
|
// player wants to destroy something
|
|
int iBuilding = atoi( args[ 1 ] );
|
|
|
|
DetonateOwnedObjectsOfType( iBuilding );
|
|
}
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "extendfreeze" ) )
|
|
{
|
|
m_flDeathTime += 2.0f;
|
|
return true;
|
|
}
|
|
else if ( FStrEq( pcmd, "show_motd" ) )
|
|
{
|
|
KeyValues *data = new KeyValues( "data" );
|
|
data->SetString( "title", "#TF_Welcome" ); // info panel title
|
|
data->SetString( "type", "1" ); // show userdata from stringtable entry
|
|
data->SetString( "msg", "motd" ); // use this stringtable entry
|
|
data->SetString( "cmd", "mapinfo" ); // exec this command if panel closed
|
|
|
|
ShowViewPortPanel( PANEL_INFO, true, data );
|
|
|
|
data->deleteThis();
|
|
}
|
|
else if ( FStrEq( pcmd, "condump_on" ) )
|
|
{
|
|
if ( !PlayerHasPowerplay() )
|
|
{
|
|
Msg("Console dumping on.\n");
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if ( args.ArgC() == 2 && GetTeam() )
|
|
{
|
|
for ( int i = 0; i < GetTeam()->GetNumPlayers(); i++ )
|
|
{
|
|
CTFPlayer *pTeamPlayer = ToTFPlayer( GetTeam()->GetPlayer(i) );
|
|
if ( pTeamPlayer )
|
|
{
|
|
pTeamPlayer->SetPowerplayEnabled( true );
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if ( SetPowerplayEnabled( true ) )
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else if ( FStrEq( pcmd, "condump_off" ) )
|
|
{
|
|
if ( !PlayerHasPowerplay() )
|
|
{
|
|
Msg("Console dumping off.\n");
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if ( args.ArgC() == 2 && GetTeam() )
|
|
{
|
|
for ( int i = 0; i < GetTeam()->GetNumPlayers(); i++ )
|
|
{
|
|
CTFPlayer *pTeamPlayer = ToTFPlayer( GetTeam()->GetPlayer(i) );
|
|
if ( pTeamPlayer )
|
|
{
|
|
pTeamPlayer->SetPowerplayEnabled( false );
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if ( SetPowerplayEnabled( false ) )
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return BaseClass::ClientCommand( args );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::SetClassMenuOpen( bool bOpen )
|
|
{
|
|
m_bIsClassMenuOpen = bOpen;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::IsClassMenuOpen( void )
|
|
{
|
|
return m_bIsClassMenuOpen;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::PlayGesture( const char *pGestureName )
|
|
{
|
|
Activity nActivity = (Activity)LookupActivity( pGestureName );
|
|
if ( nActivity != ACT_INVALID )
|
|
{
|
|
DoAnimationEvent( PLAYERANIMEVENT_CUSTOM_GESTURE, nActivity );
|
|
return true;
|
|
}
|
|
|
|
int nSequence = LookupSequence( pGestureName );
|
|
if ( nSequence != -1 )
|
|
{
|
|
DoAnimationEvent( PLAYERANIMEVENT_CUSTOM_GESTURE_SEQUENCE, nSequence );
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::PlaySpecificSequence( const char *pAnimationName )
|
|
{
|
|
Activity nActivity = (Activity)LookupActivity( pAnimationName );
|
|
if ( nActivity != ACT_INVALID )
|
|
{
|
|
DoAnimationEvent( PLAYERANIMEVENT_CUSTOM, nActivity );
|
|
return true;
|
|
}
|
|
|
|
int nSequence = LookupSequence( pAnimationName );
|
|
if ( nSequence != -1 )
|
|
{
|
|
DoAnimationEvent( PLAYERANIMEVENT_CUSTOM_SEQUENCE, nSequence );
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::CanDisguise( void )
|
|
{
|
|
if ( !IsAlive() )
|
|
return false;
|
|
|
|
if ( GetPlayerClass()->GetClassIndex() != TF_CLASS_SPY )
|
|
return false;
|
|
|
|
if ( HasItem() && GetItem()->GetItemID() == TF_ITEM_CAPTURE_FLAG )
|
|
{
|
|
HintMessage( HINT_CANNOT_DISGUISE_WITH_FLAG );
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::DetonateOwnedObjectsOfType( int iType )
|
|
{
|
|
int i;
|
|
int iNumObjects = GetObjectCount();
|
|
for ( i=0;i<iNumObjects;i++ )
|
|
{
|
|
CBaseObject *pObj = GetObject(i);
|
|
|
|
if ( pObj && pObj->GetType() == iType )
|
|
{
|
|
SpeakConceptIfAllowed( MP_CONCEPT_DETONATED_OBJECT, pObj->GetResponseRulesModifier() );
|
|
pObj->DetonateObject();
|
|
|
|
const CObjectInfo *pInfo = GetObjectInfo( iType );
|
|
|
|
if ( pInfo )
|
|
{
|
|
UTIL_LogPrintf( "\"%s<%i><%s><%s>\" triggered \"killedobject\" (object \"%s\") (weapon \"%s\") (objectowner \"%s<%i><%s><%s>\") (attacker_position \"%d %d %d\")\n",
|
|
GetPlayerName(),
|
|
GetUserID(),
|
|
GetNetworkIDString(),
|
|
GetTeam()->GetName(),
|
|
pInfo->m_pObjectName,
|
|
"pda_engineer",
|
|
GetPlayerName(),
|
|
GetUserID(),
|
|
GetNetworkIDString(),
|
|
GetTeam()->GetName(),
|
|
(int)GetAbsOrigin().x,
|
|
(int)GetAbsOrigin().y,
|
|
(int)GetAbsOrigin().z );
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StartBuildingObjectOfType( int iType )
|
|
{
|
|
// early out if we can't build this type of object
|
|
if ( CanBuild( iType ) != CB_CAN_BUILD )
|
|
return;
|
|
|
|
for ( int i = 0; i < WeaponCount(); i++)
|
|
{
|
|
CTFWeaponBase *pWpn = ( CTFWeaponBase *)GetWeapon(i);
|
|
|
|
if ( pWpn == NULL )
|
|
continue;
|
|
|
|
if ( pWpn->GetWeaponID() != TF_WEAPON_BUILDER )
|
|
continue;
|
|
|
|
CTFWeaponBuilder *pBuilder = dynamic_cast< CTFWeaponBuilder * >( pWpn );
|
|
|
|
// Is this the builder that builds the object we're looking for?
|
|
if ( pBuilder )
|
|
{
|
|
pBuilder->SetSubType( iType );
|
|
|
|
if ( GetActiveTFWeapon() == pBuilder )
|
|
{
|
|
SetActiveWeapon( NULL );
|
|
}
|
|
|
|
// try to switch to this weapon
|
|
if ( Weapon_Switch( pBuilder ) )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator )
|
|
{
|
|
if ( m_takedamage != DAMAGE_YES )
|
|
return;
|
|
|
|
CTFPlayer *pAttacker = (CTFPlayer*)ToTFPlayer( info.GetAttacker() );
|
|
if ( pAttacker )
|
|
{
|
|
// Prevent team damage here so blood doesn't appear
|
|
if ( info.GetAttacker()->IsPlayer() )
|
|
{
|
|
if ( !g_pGameRules->FPlayerCanTakeDamage( this, info.GetAttacker(), info ) )
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Save this bone for the ragdoll.
|
|
m_nForceBone = ptr->physicsbone;
|
|
|
|
SetLastHitGroup( ptr->hitgroup );
|
|
|
|
// Ignore hitboxes for all weapons except the sniper rifle
|
|
|
|
CTakeDamageInfo info_modified = info;
|
|
|
|
if ( info_modified.GetDamageType() & DMG_USE_HITLOCATIONS )
|
|
{
|
|
switch ( ptr->hitgroup )
|
|
{
|
|
case HITGROUP_HEAD:
|
|
{
|
|
CTFWeaponBase *pWpn = pAttacker->GetActiveTFWeapon();
|
|
bool bCritical = true;
|
|
|
|
if ( pWpn && !pWpn->CanFireCriticalShot( true ) )
|
|
{
|
|
bCritical = false;
|
|
}
|
|
|
|
if ( bCritical )
|
|
{
|
|
info_modified.AddDamageType( DMG_CRITICAL );
|
|
info_modified.SetDamageCustom( TF_DMG_CUSTOM_HEADSHOT );
|
|
|
|
// play the critical shot sound to the shooter
|
|
if ( pWpn )
|
|
{
|
|
pWpn->WeaponSound( BURST );
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( m_Shared.InCond( TF_COND_DISGUISED ) )
|
|
{
|
|
// no impact effects
|
|
}
|
|
else if ( m_Shared.InCond( TF_COND_INVULNERABLE ) )
|
|
{
|
|
// Make bullet impacts
|
|
g_pEffects->Ricochet( ptr->endpos - (vecDir * 8), -vecDir );
|
|
}
|
|
else
|
|
{
|
|
// Since this code only runs on the server, make sure it shows the tempents it creates.
|
|
CDisablePredictionFiltering disabler;
|
|
|
|
// This does smaller splotches on the guy and splats blood on the world.
|
|
TraceBleed( info_modified.GetDamage(), vecDir, ptr, info_modified.GetDamageType() );
|
|
}
|
|
|
|
AddMultiDamage( info_modified, this );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPlayer::TakeHealth( float flHealth, int bitsDamageType )
|
|
{
|
|
int bResult = false;
|
|
|
|
// If the bit's set, add over the max health
|
|
if ( bitsDamageType & DMG_IGNORE_MAXHEALTH )
|
|
{
|
|
int iTimeBasedDamage = g_pGameRules->Damage_GetTimeBased();
|
|
m_bitsDamageType &= ~(bitsDamageType & ~iTimeBasedDamage);
|
|
m_iHealth += flHealth;
|
|
bResult = true;
|
|
}
|
|
else
|
|
{
|
|
float flHealthToAdd = flHealth;
|
|
float flMaxHealth = GetPlayerClass()->GetMaxHealth();
|
|
|
|
// don't want to add more than we're allowed to have
|
|
if ( flHealthToAdd > flMaxHealth - m_iHealth )
|
|
{
|
|
flHealthToAdd = flMaxHealth - m_iHealth;
|
|
}
|
|
|
|
if ( flHealthToAdd <= 0 )
|
|
{
|
|
bResult = false;
|
|
}
|
|
else
|
|
{
|
|
bResult = BaseClass::TakeHealth( flHealthToAdd, bitsDamageType );
|
|
}
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::TFWeaponRemove( int iWeaponID )
|
|
{
|
|
// find the weapon that matches the id and remove it
|
|
int i;
|
|
for (i = 0; i < WeaponCount(); i++)
|
|
{
|
|
CTFWeaponBase *pWeapon = ( CTFWeaponBase *)GetWeapon( i );
|
|
if ( !pWeapon )
|
|
continue;
|
|
|
|
if ( pWeapon->GetWeaponID() != iWeaponID )
|
|
continue;
|
|
|
|
RemovePlayerItem( pWeapon );
|
|
UTIL_Remove( pWeapon );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::DropCurrentWeapon( void )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::DropFlag( void )
|
|
{
|
|
if ( HasItem() )
|
|
{
|
|
CCaptureFlag *pFlag = dynamic_cast<CCaptureFlag*>( GetItem() );
|
|
if ( pFlag )
|
|
{
|
|
pFlag->Drop( this, true, true );
|
|
IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_flag_event" );
|
|
if ( event )
|
|
{
|
|
event->SetInt( "player", entindex() );
|
|
event->SetInt( "eventtype", TF_FLAGEVENT_DROPPED );
|
|
event->SetInt( "priority", 8 );
|
|
|
|
gameeventmanager->FireEvent( event );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
EHANDLE CTFPlayer::TeamFortress_GetDisguiseTarget( int nTeam, int nClass )
|
|
{
|
|
if ( nTeam == GetTeamNumber() || nTeam == TF_SPY_UNDEFINED )
|
|
{
|
|
// we're not disguised as the enemy team
|
|
return NULL;
|
|
}
|
|
|
|
CBaseEntity *pLastTarget = m_Shared.GetDisguiseTarget(); // don't redisguise self as this person
|
|
|
|
// Find a player on the team the spy is disguised as to pretend to be
|
|
CTFPlayer *pPlayer = NULL;
|
|
|
|
// Loop through players
|
|
int i;
|
|
for ( i = 1; i <= gpGlobals->maxClients; i++ )
|
|
{
|
|
pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
|
|
if ( pPlayer )
|
|
{
|
|
if ( pPlayer == pLastTarget )
|
|
{
|
|
// choose someone else, we're trying to rid ourselves of a disguise as this one
|
|
continue;
|
|
}
|
|
|
|
// First, try to find a player with the same color AND skin
|
|
if ( pPlayer->GetTeamNumber() == nTeam && pPlayer->GetPlayerClass()->GetClassIndex() == nClass )
|
|
{
|
|
return pPlayer;
|
|
}
|
|
}
|
|
}
|
|
|
|
// we didn't find someone with the same skin, so just find someone with the same color
|
|
for ( i = 1; i <= gpGlobals->maxClients; i++ )
|
|
{
|
|
pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
|
|
if ( pPlayer )
|
|
{
|
|
if ( pPlayer->GetTeamNumber() == nTeam )
|
|
{
|
|
return pPlayer;
|
|
}
|
|
}
|
|
}
|
|
|
|
// we didn't find anyone
|
|
return NULL;
|
|
}
|
|
|
|
static float DamageForce( const Vector &size, float damage, float scale )
|
|
{
|
|
float force = damage * ((48 * 48 * 82.0) / (size.x * size.y * size.z)) * scale;
|
|
|
|
if ( force > 1000.0)
|
|
{
|
|
force = 1000.0;
|
|
}
|
|
|
|
return force;
|
|
}
|
|
|
|
ConVar tf_debug_damage( "tf_debug_damage", "0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPlayer::OnTakeDamage( const CTakeDamageInfo &inputInfo )
|
|
{
|
|
CTakeDamageInfo info = inputInfo;
|
|
|
|
if ( GetFlags() & FL_GODMODE )
|
|
return 0;
|
|
|
|
if ( IsInCommentaryMode() )
|
|
return 0;
|
|
|
|
if ( m_debugOverlays & OVERLAY_BUDDHA_MODE )
|
|
{
|
|
if ((m_iHealth - info.GetDamage()) <= 0)
|
|
{
|
|
m_iHealth = 1;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Early out if there's no damage
|
|
if ( !info.GetDamage() )
|
|
return 0;
|
|
|
|
if ( !IsAlive() )
|
|
return 0;
|
|
|
|
int iHealthBefore = GetHealth();
|
|
|
|
bool bDebug = tf_debug_damage.GetBool();
|
|
if ( bDebug )
|
|
{
|
|
Warning( "%s taking damage from %s, via %s. Damage: %.2f\n", GetDebugName(), info.GetInflictor() ? info.GetInflictor()->GetDebugName() : "Unknown Inflictor", info.GetAttacker() ? info.GetAttacker()->GetDebugName() : "Unknown Attacker", info.GetDamage() );
|
|
}
|
|
|
|
// Make sure the player can take damage from the attacking entity
|
|
if ( !g_pGameRules->FPlayerCanTakeDamage( this, info.GetAttacker(), info ) )
|
|
{
|
|
if ( bDebug )
|
|
{
|
|
Warning( " ABORTED: Player can't take damage from that attacker.\n" );
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
AddDamagerToHistory( info.GetAttacker() );
|
|
|
|
// keep track of amount of damage last sustained
|
|
m_lastDamageAmount = info.GetDamage();
|
|
m_LastDamageType = info.GetDamageType();
|
|
|
|
if ( IsPlayerClass( TF_CLASS_SPY ) && !( info.GetDamageType() & DMG_FALL ) )
|
|
{
|
|
m_Shared.NoteLastDamageTime( m_lastDamageAmount );
|
|
}
|
|
|
|
// if this is our own rocket, scale down the damage
|
|
if ( IsPlayerClass( TF_CLASS_SOLDIER ) && info.GetAttacker() == this )
|
|
{
|
|
float flDamage = info.GetDamage() * tf_damagescale_self_soldier.GetFloat();
|
|
info.SetDamage( flDamage );
|
|
}
|
|
|
|
// Save damage force for ragdolls.
|
|
m_vecTotalBulletForce = info.GetDamageForce();
|
|
m_vecTotalBulletForce.x = clamp( m_vecTotalBulletForce.x, -15000.0f, 15000.0f );
|
|
m_vecTotalBulletForce.y = clamp( m_vecTotalBulletForce.y, -15000.0f, 15000.0f );
|
|
m_vecTotalBulletForce.z = clamp( m_vecTotalBulletForce.z, -15000.0f, 15000.0f );
|
|
|
|
int bTookDamage = 0;
|
|
|
|
int bitsDamage = inputInfo.GetDamageType();
|
|
|
|
// If we're invulnerable, force ourselves to only take damage events only, so we still get pushed
|
|
if ( m_Shared.InCond( TF_COND_INVULNERABLE ) )
|
|
{
|
|
bool bAllowDamage = false;
|
|
|
|
// check to see if our attacker is a trigger_hurt entity (and allow it to kill us even if we're invuln)
|
|
CBaseEntity *pAttacker = info.GetAttacker();
|
|
if ( pAttacker && pAttacker->IsSolidFlagSet( FSOLID_TRIGGER ) )
|
|
{
|
|
CTriggerHurt *pTrigger = dynamic_cast<CTriggerHurt *>( pAttacker );
|
|
if ( pTrigger )
|
|
{
|
|
bAllowDamage = true;
|
|
}
|
|
}
|
|
|
|
if ( !bAllowDamage )
|
|
{
|
|
int iOldTakeDamage = m_takedamage;
|
|
m_takedamage = DAMAGE_EVENTS_ONLY;
|
|
// NOTE: Deliberately skip base player OnTakeDamage, because we don't want all the stuff it does re: suit voice
|
|
CBaseCombatCharacter::OnTakeDamage( info );
|
|
m_takedamage = iOldTakeDamage;
|
|
|
|
// Burn sounds are handled in ConditionThink()
|
|
if ( !(bitsDamage & DMG_BURN ) )
|
|
{
|
|
SpeakConceptIfAllowed( MP_CONCEPT_HURT );
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// If we're not damaging ourselves, apply randomness
|
|
if ( info.GetAttacker() != this && !(bitsDamage & (DMG_DROWN | DMG_FALL)) )
|
|
{
|
|
float flDamage = 0;
|
|
if ( bitsDamage & DMG_CRITICAL )
|
|
{
|
|
if ( bDebug )
|
|
{
|
|
Warning( " CRITICAL!\n");
|
|
}
|
|
|
|
flDamage = info.GetDamage() * TF_DAMAGE_CRIT_MULTIPLIER;
|
|
|
|
// Show the attacker, unless the target is a disguised spy
|
|
if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() && !m_Shared.InCond( TF_COND_DISGUISED ) )
|
|
{
|
|
CEffectData data;
|
|
data.m_nHitBox = GetParticleSystemIndex( "crit_text" );
|
|
data.m_vOrigin = WorldSpaceCenter() + Vector(0,0,32);
|
|
data.m_vAngles = vec3_angle;
|
|
data.m_nEntIndex = 0;
|
|
|
|
CSingleUserRecipientFilter filter( (CBasePlayer*)info.GetAttacker() );
|
|
te->DispatchEffect( filter, 0.0, data.m_vOrigin, "ParticleEffect", data );
|
|
|
|
EmitSound_t params;
|
|
params.m_flSoundTime = 0;
|
|
params.m_pSoundName = "TFPlayer.CritHit";
|
|
EmitSound( filter, info.GetAttacker()->entindex(), params );
|
|
}
|
|
|
|
// Burn sounds are handled in ConditionThink()
|
|
if ( !(bitsDamage & DMG_BURN ) )
|
|
{
|
|
SpeakConceptIfAllowed( MP_CONCEPT_HURT, "damagecritical:1" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float flRandomDamage = info.GetDamage() * tf_damage_range.GetFloat();
|
|
if ( tf_damage_lineardist.GetBool() )
|
|
{
|
|
float flBaseDamage = info.GetDamage() - flRandomDamage;
|
|
flDamage = flBaseDamage + RandomFloat( 0, flRandomDamage * 2 );
|
|
}
|
|
else
|
|
{
|
|
float flMin = 0.25;
|
|
float flMax = 0.75;
|
|
float flCenter = 0.5;
|
|
|
|
if ( bitsDamage & DMG_USEDISTANCEMOD )
|
|
{
|
|
float flDistance = MAX( 1.0, (WorldSpaceCenter() - info.GetAttacker()->WorldSpaceCenter()).Length() );
|
|
float flOptimalDistance = 512.0;
|
|
|
|
flCenter = RemapValClamped( flDistance / flOptimalDistance, 0.0, 2.0, 1.0, 0.0 );
|
|
if ( bitsDamage & DMG_NOCLOSEDISTANCEMOD )
|
|
{
|
|
if ( flCenter > 0.5 )
|
|
{
|
|
// Reduce the damage bonus at close range
|
|
flCenter = RemapVal( flCenter, 0.5, 1.0, 0.5, 0.65 );
|
|
}
|
|
}
|
|
flMin = MAX( 0.0, flCenter - 0.25 );
|
|
flMax = MIN( 1.0, flCenter + 0.25 );
|
|
|
|
if ( bDebug )
|
|
{
|
|
Warning(" RANDOM: Dist %.2f, Ctr: %.2f, Min: %.2f, Max: %.2f\n", flDistance, flCenter, flMin, flMax );
|
|
}
|
|
}
|
|
|
|
//Msg("Range: %.2f - %.2f\n", flMin, flMax );
|
|
float flRandomVal = tf_damage_disablespread.GetBool() ? flCenter : RandomFloat( flMin, flMax );
|
|
|
|
if ( flRandomVal > 0.5 )
|
|
{
|
|
// Rocket launcher & Scattergun have different short range bonuses
|
|
if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() )
|
|
{
|
|
CTFWeaponBase *pWeapon = ToTFPlayer( info.GetAttacker() )->GetActiveTFWeapon();
|
|
if ( pWeapon )
|
|
{
|
|
if ( pWeapon->GetWeaponID() == TF_WEAPON_ROCKETLAUNCHER )
|
|
{
|
|
// Rocket launcher only has half the bonus of the other weapons at short range
|
|
flRandomDamage *= 0.5;
|
|
}
|
|
else if ( pWeapon->GetWeaponID() == TF_WEAPON_SCATTERGUN )
|
|
{
|
|
// Scattergun gets 50% bonus of other weapons at short range
|
|
flRandomDamage *= 1.5;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
float flOut = SimpleSplineRemapValClamped( flRandomVal, 0, 1, -flRandomDamage, flRandomDamage );
|
|
flDamage = info.GetDamage() + flOut;
|
|
|
|
/*
|
|
for ( float flVal = flMin; flVal <= flMax; flVal += 0.05 )
|
|
{
|
|
float flOut = SimpleSplineRemapValClamped( flVal, 0, 1, -flRandomDamage, flRandomDamage );
|
|
Msg("Val: %.2f, Out: %.2f, Dmg: %.2f\n", flVal, flOut, info.GetDamage() + flOut );
|
|
}
|
|
*/
|
|
}
|
|
|
|
// Burn sounds are handled in ConditionThink()
|
|
if ( !(bitsDamage & DMG_BURN ) )
|
|
{
|
|
SpeakConceptIfAllowed( MP_CONCEPT_HURT );
|
|
}
|
|
}
|
|
|
|
info.SetDamage( flDamage );
|
|
}
|
|
|
|
// NOTE: Deliberately skip base player OnTakeDamage, because we don't want all the stuff it does re: suit voice
|
|
bTookDamage = CBaseCombatCharacter::OnTakeDamage( info );
|
|
|
|
// Early out if the base class took no damage
|
|
if ( !bTookDamage )
|
|
{
|
|
if ( bDebug )
|
|
{
|
|
Warning( " ABORTED: Player failed to take the damage.\n" );
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if ( bDebug )
|
|
{
|
|
Warning( " DEALT: Player took %.2f damage.\n", info.GetDamage() );
|
|
Warning( " HEALTH LEFT: %d\n", GetHealth() );
|
|
}
|
|
|
|
// Send the damage message to the client for the hud damage indicator
|
|
// Don't do this for damage types that don't use the indicator
|
|
if ( !(bitsDamage & (DMG_DROWN | DMG_FALL | DMG_BURN) ) )
|
|
{
|
|
// Try and figure out where the damage is coming from
|
|
Vector vecDamageOrigin = info.GetReportedPosition();
|
|
|
|
// If we didn't get an origin to use, try using the attacker's origin
|
|
if ( vecDamageOrigin == vec3_origin && info.GetInflictor() )
|
|
{
|
|
vecDamageOrigin = info.GetInflictor()->GetAbsOrigin();
|
|
}
|
|
|
|
CSingleUserRecipientFilter user( this );
|
|
UserMessageBegin( user, "Damage" );
|
|
WRITE_BYTE( clamp( (int)info.GetDamage(), 0, 255 ) );
|
|
WRITE_VEC3COORD( vecDamageOrigin );
|
|
MessageEnd();
|
|
}
|
|
|
|
// add to the damage total for clients, which will be sent as a single
|
|
// message at the end of the frame
|
|
// todo: remove after combining shotgun blasts?
|
|
if ( info.GetInflictor() && info.GetInflictor()->edict() )
|
|
{
|
|
m_DmgOrigin = info.GetInflictor()->GetAbsOrigin();
|
|
}
|
|
|
|
m_DmgTake += (int)info.GetDamage();
|
|
|
|
// Reset damage time countdown for each type of time based damage player just sustained
|
|
for (int i = 0; i < CDMG_TIMEBASED; i++)
|
|
{
|
|
// Make sure the damage type is really time-based.
|
|
// This is kind of hacky but necessary until we setup DamageType as an enum.
|
|
int iDamage = ( DMG_PARALYZE << i );
|
|
if ( ( info.GetDamageType() & iDamage ) && g_pGameRules->Damage_IsTimeBased( iDamage ) )
|
|
{
|
|
m_rgbTimeBasedDamage[i] = 0;
|
|
}
|
|
}
|
|
|
|
// Display any effect associate with this damage type
|
|
DamageEffect( info.GetDamage(),bitsDamage );
|
|
|
|
m_bitsDamageType |= bitsDamage; // Save this so we can report it to the client
|
|
m_bitsHUDDamage = -1; // make sure the damage bits get resent
|
|
|
|
m_Local.m_vecPunchAngle.SetX( -2 );
|
|
|
|
// Do special explosion damage effect
|
|
if ( bitsDamage & DMG_BLAST )
|
|
{
|
|
OnDamagedByExplosion( info );
|
|
}
|
|
|
|
PainSound( info );
|
|
|
|
PlayFlinch( info );
|
|
|
|
// Detect drops below 25% health and restart expression, so that characters look worried.
|
|
int iHealthBoundary = (GetMaxHealth() * 0.25);
|
|
if ( GetHealth() <= iHealthBoundary && iHealthBefore > iHealthBoundary )
|
|
{
|
|
ClearExpression();
|
|
}
|
|
|
|
CTF_GameStats.Event_PlayerDamage( this, info, iHealthBefore - GetHealth() );
|
|
|
|
return bTookDamage;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::DamageEffect(float flDamage, int fDamageType)
|
|
{
|
|
bool bDisguised = m_Shared.InCond( TF_COND_DISGUISED );
|
|
|
|
if (fDamageType & DMG_CRUSH)
|
|
{
|
|
//Red damage indicator
|
|
color32 red = {128,0,0,128};
|
|
UTIL_ScreenFade( this, red, 1.0f, 0.1f, FFADE_IN );
|
|
}
|
|
else if (fDamageType & DMG_DROWN)
|
|
{
|
|
//Red damage indicator
|
|
color32 blue = {0,0,128,128};
|
|
UTIL_ScreenFade( this, blue, 1.0f, 0.1f, FFADE_IN );
|
|
}
|
|
else if (fDamageType & DMG_SLASH)
|
|
{
|
|
if ( !bDisguised )
|
|
{
|
|
// If slash damage shoot some blood
|
|
SpawnBlood(EyePosition(), g_vecAttackDir, BloodColor(), flDamage);
|
|
}
|
|
}
|
|
else if ( fDamageType & DMG_BULLET )
|
|
{
|
|
if ( !bDisguised )
|
|
{
|
|
EmitSound( "Flesh.BulletImpact" );
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : collisionGroup -
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::ShouldCollide( int collisionGroup, int contentsMask ) const
|
|
{
|
|
if ( ( ( collisionGroup == COLLISION_GROUP_PLAYER_MOVEMENT ) && tf_avoidteammates.GetBool() ) ||
|
|
collisionGroup == TFCOLLISION_GROUP_ROCKETS )
|
|
{
|
|
switch( GetTeamNumber() )
|
|
{
|
|
case TF_TEAM_RED:
|
|
if ( !( contentsMask & CONTENTS_REDTEAM ) )
|
|
return false;
|
|
break;
|
|
|
|
case TF_TEAM_BLUE:
|
|
if ( !( contentsMask & CONTENTS_BLUETEAM ) )
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
return BaseClass::ShouldCollide( collisionGroup, contentsMask );
|
|
}
|
|
|
|
//---------------------------------------
|
|
// Is the player the passed player class?
|
|
//---------------------------------------
|
|
bool CTFPlayer::IsPlayerClass( int iClass ) const
|
|
{
|
|
const CTFPlayerClass *pClass = &m_PlayerClass;
|
|
|
|
if ( !pClass )
|
|
return false;
|
|
|
|
return ( pClass->IsClass( iClass ) );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::CommitSuicide( bool bExplode /* = false */, bool bForce /*= false*/ )
|
|
{
|
|
// Don't suicide if we haven't picked a class for the first time, or we're not in active state
|
|
if ( IsPlayerClass( TF_CLASS_UNDEFINED ) || !m_Shared.InState( TF_STATE_ACTIVE ) )
|
|
return;
|
|
|
|
// Don't suicide during the "bonus time" if we're not on the winning team
|
|
if ( !bForce && TFGameRules()->State_Get() == GR_STATE_TEAM_WIN &&
|
|
GetTeamNumber() != TFGameRules()->GetWinningTeam() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_iSuicideCustomKillFlags = TF_DMG_CUSTOM_SUICIDE;
|
|
|
|
BaseClass::CommitSuicide( bExplode, bForce );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : &info -
|
|
// Output : int
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPlayer::OnTakeDamage_Alive( const CTakeDamageInfo &info )
|
|
{
|
|
// Grab the vector of the incoming attack.
|
|
// (Pretend that the inflictor is a little lower than it really is, so the body will tend to fly upward a bit).
|
|
Vector vecDir = vec3_origin;
|
|
if ( info.GetInflictor() )
|
|
{
|
|
vecDir = info.GetInflictor()->WorldSpaceCenter() - Vector ( 0.0f, 0.0f, 10.0f ) - WorldSpaceCenter();
|
|
VectorNormalize( vecDir );
|
|
}
|
|
g_vecAttackDir = vecDir;
|
|
|
|
// Do the damage.
|
|
m_bitsDamageType |= info.GetDamageType();
|
|
|
|
bool bIgniting = false;
|
|
|
|
if ( m_takedamage != DAMAGE_EVENTS_ONLY )
|
|
{
|
|
// Start burning if we took ignition damage
|
|
bIgniting = ( ( info.GetDamageType() & DMG_IGNITE ) && ( GetWaterLevel() < WL_Waist ) );
|
|
|
|
if ( info.GetDamage() == 0.0f )
|
|
return 0;
|
|
|
|
// Take damage - round to the nearest integer.
|
|
m_iHealth -= ( info.GetDamage() + 0.5f );
|
|
}
|
|
|
|
m_flLastDamageTime = gpGlobals->curtime;
|
|
|
|
// Apply a damage force.
|
|
CBaseEntity *pAttacker = info.GetAttacker();
|
|
if ( !pAttacker )
|
|
return 0;
|
|
|
|
if ( ( info.GetDamageType() & DMG_PREVENT_PHYSICS_FORCE ) == 0 )
|
|
{
|
|
if ( info.GetInflictor() && ( GetMoveType() == MOVETYPE_WALK ) &&
|
|
( !pAttacker->IsSolidFlagSet( FSOLID_TRIGGER ) ) &&
|
|
( !m_Shared.InCond( TF_COND_DISGUISED ) ) )
|
|
{
|
|
Vector vecForce;
|
|
vecForce.Init();
|
|
if ( info.GetAttacker() == this )
|
|
{
|
|
if ( IsPlayerClass( TF_CLASS_SOLDIER ) )
|
|
{
|
|
vecForce = vecDir * -DamageForce( WorldAlignSize(), info.GetDamage(), tf_damageforcescale_self_soldier.GetFloat() );
|
|
}
|
|
else
|
|
{
|
|
vecForce = vecDir * -DamageForce( WorldAlignSize(), info.GetDamage(), DAMAGE_FORCE_SCALE_SELF );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Sentryguns push a lot harder
|
|
if ( (info.GetDamageType() & DMG_BULLET) && info.GetInflictor()->IsBaseObject() )
|
|
{
|
|
vecForce = vecDir * -DamageForce( WorldAlignSize(), info.GetDamage(), 16 );
|
|
}
|
|
else
|
|
{
|
|
vecForce = vecDir * -DamageForce( WorldAlignSize(), info.GetDamage(), tf_damageforcescale_other.GetFloat() );
|
|
|
|
if ( IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) )
|
|
{
|
|
// Heavies take less push from non sentryguns
|
|
vecForce *= 0.5;
|
|
}
|
|
}
|
|
}
|
|
|
|
ApplyAbsVelocityImpulse( vecForce );
|
|
}
|
|
}
|
|
|
|
if ( bIgniting )
|
|
{
|
|
m_Shared.Burn( ToTFPlayer( pAttacker ) );
|
|
}
|
|
|
|
// Fire a global game event - "player_hurt"
|
|
IGameEvent * event = gameeventmanager->CreateEvent( "player_hurt" );
|
|
if ( event )
|
|
{
|
|
event->SetInt( "userid", GetUserID() );
|
|
event->SetInt( "health", MAX( 0, m_iHealth ) );
|
|
|
|
// HLTV event priority, not transmitted
|
|
event->SetInt( "priority", 5 );
|
|
|
|
// Hurt by another player.
|
|
if ( pAttacker->IsPlayer() )
|
|
{
|
|
CBasePlayer *pPlayer = ToBasePlayer( pAttacker );
|
|
event->SetInt( "attacker", pPlayer->GetUserID() );
|
|
}
|
|
// Hurt by world.
|
|
else
|
|
{
|
|
event->SetInt( "attacker", 0 );
|
|
}
|
|
|
|
gameeventmanager->FireEvent( event );
|
|
}
|
|
|
|
if ( pAttacker != this && pAttacker->IsPlayer() )
|
|
{
|
|
ToTFPlayer( pAttacker )->RecordDamageEvent( info, (m_iHealth <= 0) );
|
|
}
|
|
|
|
//No bleeding while invul or disguised.
|
|
bool bBleed = ( m_Shared.InCond( TF_COND_DISGUISED ) == false && m_Shared.InCond( TF_COND_INVULNERABLE ) == false );
|
|
if ( bBleed && pAttacker->IsPlayer() )
|
|
{
|
|
CTFWeaponBase *pWeapon = ToTFPlayer( pAttacker )->GetActiveTFWeapon();
|
|
if ( pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_FLAMETHROWER )
|
|
{
|
|
bBleed = false;
|
|
}
|
|
}
|
|
|
|
if ( bBleed )
|
|
{
|
|
Vector vDamagePos = info.GetDamagePosition();
|
|
|
|
if ( vDamagePos == vec3_origin )
|
|
{
|
|
vDamagePos = WorldSpaceCenter();
|
|
}
|
|
|
|
CPVSFilter filter( vDamagePos );
|
|
TE_TFBlood( filter, 0.0, vDamagePos, -vecDir, entindex() );
|
|
}
|
|
|
|
// Done.
|
|
return 1;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Adds this damager to the history list of people who damaged player
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::AddDamagerToHistory( EHANDLE hDamager )
|
|
{
|
|
// sanity check: ignore damager if it is on our team. (Catch-all for
|
|
// damaging self in rocket jumps, etc.)
|
|
CTFPlayer *pDamager = ToTFPlayer( hDamager );
|
|
if ( !pDamager || ( pDamager->GetTeam() == GetTeam() ) )
|
|
return;
|
|
|
|
// If this damager is different from the most recent damager, shift the
|
|
// damagers down and drop the oldest damager. (If this damager is already
|
|
// the most recent, we will just update the damage time but not remove
|
|
// other damagers from history.)
|
|
if ( m_DamagerHistory[0].hDamager != hDamager )
|
|
{
|
|
for ( int i = 1; i < ARRAYSIZE( m_DamagerHistory ); i++ )
|
|
{
|
|
m_DamagerHistory[i] = m_DamagerHistory[i-1];
|
|
}
|
|
}
|
|
// set this damager as most recent and note the time
|
|
m_DamagerHistory[0].hDamager = hDamager;
|
|
m_DamagerHistory[0].flTimeDamage = gpGlobals->curtime;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Clears damager history
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::ClearDamagerHistory()
|
|
{
|
|
for ( int i = 0; i < ARRAYSIZE( m_DamagerHistory ); i++ )
|
|
{
|
|
m_DamagerHistory[i].Reset();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : &info -
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::ShouldGib( const CTakeDamageInfo &info )
|
|
{
|
|
// Check to see if we should allow players to gib.
|
|
if ( !tf_playergib.GetBool() )
|
|
return false;
|
|
|
|
if ( ( ( info.GetDamageType() & DMG_BLAST ) != 0 ) || ( ( info.GetDamageType() & DMG_HALF_FALLOFF ) != 0 ) )
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info )
|
|
{
|
|
BaseClass::Event_KilledOther( pVictim, info );
|
|
|
|
if ( pVictim->IsPlayer() )
|
|
{
|
|
CTFPlayer *pTFVictim = ToTFPlayer(pVictim);
|
|
|
|
// Custom death handlers
|
|
const char *pszCustomDeath = "customdeath:none";
|
|
if ( info.GetAttacker() && info.GetAttacker()->IsBaseObject() )
|
|
{
|
|
pszCustomDeath = "customdeath:sentrygun";
|
|
}
|
|
else if ( info.GetInflictor() && info.GetInflictor()->IsBaseObject() )
|
|
{
|
|
pszCustomDeath = "customdeath:sentrygun";
|
|
}
|
|
else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_HEADSHOT )
|
|
{
|
|
pszCustomDeath = "customdeath:headshot";
|
|
}
|
|
else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BACKSTAB )
|
|
{
|
|
pszCustomDeath = "customdeath:backstab";
|
|
}
|
|
else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BURNING )
|
|
{
|
|
pszCustomDeath = "customdeath:burning";
|
|
}
|
|
|
|
// Revenge handler
|
|
const char *pszDomination = "domination:none";
|
|
if ( pTFVictim->GetDeathFlags() & (TF_DEATH_REVENGE|TF_DEATH_ASSISTER_REVENGE) )
|
|
{
|
|
pszDomination = "domination:revenge";
|
|
}
|
|
else if ( pTFVictim->GetDeathFlags() & TF_DEATH_DOMINATION )
|
|
{
|
|
pszDomination = "domination:dominated";
|
|
}
|
|
|
|
CFmtStrN<128> modifiers( "%s,%s,victimclass:%s", pszCustomDeath, pszDomination, g_aPlayerClassNames_NonLocalized[ pTFVictim->GetPlayerClass()->GetClassIndex() ] );
|
|
SpeakConceptIfAllowed( MP_CONCEPT_KILLED_PLAYER, modifiers );
|
|
}
|
|
else
|
|
{
|
|
if ( pVictim->IsBaseObject() )
|
|
{
|
|
CBaseObject *pObject = dynamic_cast<CBaseObject *>( pVictim );
|
|
SpeakConceptIfAllowed( MP_CONCEPT_KILLED_OBJECT, pObject->GetResponseRulesModifier() );
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::Event_Killed( const CTakeDamageInfo &info )
|
|
{
|
|
SpeakConceptIfAllowed( MP_CONCEPT_DIED );
|
|
|
|
StateTransition( TF_STATE_DYING ); // Transition into the dying state.
|
|
|
|
CTFPlayer *pPlayerAttacker = NULL;
|
|
if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() )
|
|
{
|
|
pPlayerAttacker = ToTFPlayer( info.GetAttacker() );
|
|
}
|
|
|
|
bool bDisguised = m_Shared.InCond( TF_COND_DISGUISED );
|
|
// we want the rag doll to burn if the player was burning and was not a pryo (who only burns momentarily)
|
|
bool bBurning = m_Shared.InCond( TF_COND_BURNING ) && ( TF_CLASS_PYRO != GetPlayerClass()->GetClassIndex() );
|
|
|
|
// Remove all conditions...
|
|
m_Shared.RemoveAllCond( NULL );
|
|
|
|
// Reset our model if we were disguised
|
|
if ( bDisguised )
|
|
{
|
|
UpdateModel();
|
|
}
|
|
|
|
RemoveTeleportEffect();
|
|
|
|
// Stop being invisible
|
|
m_Shared.RemoveCond( TF_COND_STEALTHED );
|
|
|
|
// Drop a pack with their leftover ammo
|
|
DropAmmoPack();
|
|
|
|
// If the player has a capture flag and was killed by another player, award that player a defense
|
|
if ( HasItem() && pPlayerAttacker && ( pPlayerAttacker != this ) )
|
|
{
|
|
CCaptureFlag *pCaptureFlag = dynamic_cast<CCaptureFlag *>( GetItem() );
|
|
if ( pCaptureFlag )
|
|
{
|
|
IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_flag_event" );
|
|
if ( event )
|
|
{
|
|
event->SetInt( "player", pPlayerAttacker->entindex() );
|
|
event->SetInt( "eventtype", TF_FLAGEVENT_DEFEND );
|
|
event->SetInt( "priority", 8 );
|
|
gameeventmanager->FireEvent( event );
|
|
}
|
|
CTF_GameStats.Event_PlayerDefendedPoint( pPlayerAttacker );
|
|
}
|
|
}
|
|
|
|
// Remove all items...
|
|
RemoveAllItems( true );
|
|
|
|
for ( int iWeapon = 0; iWeapon < TF_PLAYER_WEAPON_COUNT; ++iWeapon )
|
|
{
|
|
CTFWeaponBase *pWeapon = (CTFWeaponBase *)GetWeapon( iWeapon );
|
|
|
|
if ( pWeapon )
|
|
{
|
|
pWeapon->WeaponReset();
|
|
}
|
|
}
|
|
|
|
if ( GetActiveWeapon() )
|
|
{
|
|
GetActiveWeapon()->SendViewModelAnim( ACT_IDLE );
|
|
GetActiveWeapon()->Holster();
|
|
SetActiveWeapon( NULL );
|
|
}
|
|
|
|
ClearZoomOwner();
|
|
|
|
m_vecLastDeathPosition = GetAbsOrigin();
|
|
|
|
CTakeDamageInfo info_modified = info;
|
|
|
|
// Ragdoll, gib, or death animation.
|
|
bool bRagdoll = true;
|
|
bool bGib = false;
|
|
|
|
// See if we should gib.
|
|
if ( ShouldGib( info ) )
|
|
{
|
|
bGib = true;
|
|
bRagdoll = false;
|
|
}
|
|
else
|
|
// See if we should play a custom death animation.
|
|
{
|
|
if ( PlayDeathAnimation( info, info_modified ) )
|
|
{
|
|
bRagdoll = false;
|
|
}
|
|
}
|
|
|
|
// show killer in death cam mode
|
|
// chopped down version of SetObserverTarget without the team check
|
|
if( pPlayerAttacker )
|
|
{
|
|
// See if we were killed by a sentrygun. If so, look at that instead of the player
|
|
if ( info.GetInflictor() && info.GetInflictor()->IsBaseObject() )
|
|
{
|
|
// Catches the case where we're killed directly by the sentrygun (i.e. bullets)
|
|
// Look at the sentrygun
|
|
m_hObserverTarget.Set( info.GetInflictor() );
|
|
}
|
|
// See if we were killed by a projectile emitted from a base object. The attacker
|
|
// will still be the owner of that object, but we want the deathcam to point to the
|
|
// object itself.
|
|
else if ( info.GetInflictor() && info.GetInflictor()->GetOwnerEntity() &&
|
|
info.GetInflictor()->GetOwnerEntity()->IsBaseObject() )
|
|
{
|
|
m_hObserverTarget.Set( info.GetInflictor()->GetOwnerEntity() );
|
|
}
|
|
else
|
|
{
|
|
// Look at the player
|
|
m_hObserverTarget.Set( info.GetAttacker() );
|
|
}
|
|
|
|
// reset fov to default
|
|
SetFOV( this, 0 );
|
|
}
|
|
else if ( info.GetAttacker() && info.GetAttacker()->IsBaseObject() )
|
|
{
|
|
// Catches the case where we're killed by entities spawned by the sentrygun (i.e. rockets)
|
|
// Look at the sentrygun.
|
|
m_hObserverTarget.Set( info.GetAttacker() );
|
|
}
|
|
else
|
|
{
|
|
m_hObserverTarget.Set( NULL );
|
|
}
|
|
|
|
if ( info_modified.GetDamageCustom() == TF_DMG_CUSTOM_SUICIDE )
|
|
{
|
|
// if this was suicide, recalculate attacker to see if we want to award the kill to a recent damager
|
|
info_modified.SetAttacker( TFGameRules()->GetDeathScorer( info.GetAttacker(), info.GetInflictor(), this ) );
|
|
}
|
|
|
|
BaseClass::Event_Killed( info_modified );
|
|
|
|
CTFPlayer *pInflictor = ToTFPlayer( info.GetInflictor() );
|
|
if ( ( TF_DMG_CUSTOM_HEADSHOT == info.GetDamageCustom() ) && pInflictor )
|
|
{
|
|
CTF_GameStats.Event_Headshot( pInflictor );
|
|
}
|
|
else if ( ( TF_DMG_CUSTOM_BACKSTAB == info.GetDamageCustom() ) && pInflictor )
|
|
{
|
|
CTF_GameStats.Event_Backstab( pInflictor );
|
|
}
|
|
|
|
// Create the ragdoll entity.
|
|
if ( bGib || bRagdoll )
|
|
{
|
|
CreateRagdollEntity( bGib, bBurning );
|
|
}
|
|
|
|
// Don't overflow the value for this.
|
|
m_iHealth = 0;
|
|
|
|
// If we died in sudden death and we're an engineer, explode our buildings
|
|
if ( IsPlayerClass( TF_CLASS_ENGINEER ) && TFGameRules()->InStalemate() )
|
|
{
|
|
for (int i = GetObjectCount()-1; i >= 0; i--)
|
|
{
|
|
CBaseObject *obj = GetObject(i);
|
|
Assert( obj );
|
|
|
|
if ( obj )
|
|
{
|
|
obj->DetonateObject();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::PlayDeathAnimation( const CTakeDamageInfo &info, CTakeDamageInfo &info_modified )
|
|
{
|
|
// No supporting this for the initial release.
|
|
return false;
|
|
|
|
if ( SelectWeightedSequence( ACT_DIESIMPLE ) == -1 )
|
|
return false;
|
|
|
|
// Get the attacking player.
|
|
CTFPlayer *pAttacker = (CTFPlayer*)ToTFPlayer( info.GetAttacker() );
|
|
if ( !pAttacker )
|
|
return false;
|
|
|
|
bool bPlayDeathAnim = false;
|
|
|
|
// Check for a sniper headshot. (Currently only on Heavy.)
|
|
if ( pAttacker->GetPlayerClass()->IsClass( TF_CLASS_SNIPER ) && ( info.GetDamageCustom() == TF_DMG_CUSTOM_HEADSHOT ) )
|
|
{
|
|
if ( GetPlayerClass()->IsClass( TF_CLASS_HEAVYWEAPONS ) )
|
|
{
|
|
bPlayDeathAnim = true;
|
|
}
|
|
}
|
|
// Check for a spy backstab. (Currently only on Sniper.)
|
|
else if ( pAttacker->GetPlayerClass()->IsClass( TF_CLASS_SPY ) && ( info.GetDamageCustom() == TF_DMG_CUSTOM_BACKSTAB ) )
|
|
{
|
|
if ( GetPlayerClass()->IsClass( TF_CLASS_SNIPER ) )
|
|
{
|
|
bPlayDeathAnim = true;
|
|
}
|
|
}
|
|
|
|
// Play death animation?
|
|
if ( bPlayDeathAnim )
|
|
{
|
|
info_modified.SetDamageType( info_modified.GetDamageType() | DMG_REMOVENORAGDOLL | DMG_PREVENT_PHYSICS_FORCE );
|
|
|
|
SetAbsVelocity( vec3_origin );
|
|
DoAnimationEvent( PLAYERANIMEVENT_DIE );
|
|
|
|
// No ragdoll yet.
|
|
if ( m_hRagdoll.Get() )
|
|
{
|
|
UTIL_Remove( m_hRagdoll );
|
|
}
|
|
}
|
|
|
|
return bPlayDeathAnim;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : *pWeapon -
|
|
// &vecOrigin -
|
|
// &vecAngles -
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::CalculateAmmoPackPositionAndAngles( CTFWeaponBase *pWeapon, Vector &vecOrigin, QAngle &vecAngles )
|
|
{
|
|
// Look up the hand and weapon bones.
|
|
int iHandBone = LookupBone( "weapon_bone" );
|
|
if ( iHandBone == -1 )
|
|
return false;
|
|
|
|
GetBonePosition( iHandBone, vecOrigin, vecAngles );
|
|
|
|
// Draw the position and angles.
|
|
Vector vecDebugForward2, vecDebugRight2, vecDebugUp2;
|
|
AngleVectors( vecAngles, &vecDebugForward2, &vecDebugRight2, &vecDebugUp2 );
|
|
|
|
/*
|
|
NDebugOverlay::Line( vecOrigin, ( vecOrigin + vecDebugForward2 * 25.0f ), 255, 0, 0, false, 30.0f );
|
|
NDebugOverlay::Line( vecOrigin, ( vecOrigin + vecDebugRight2 * 25.0f ), 0, 255, 0, false, 30.0f );
|
|
NDebugOverlay::Line( vecOrigin, ( vecOrigin + vecDebugUp2 * 25.0f ), 0, 0, 255, false, 30.0f );
|
|
*/
|
|
|
|
VectorAngles( vecDebugUp2, vecAngles );
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// NOTE: If we don't let players drop ammo boxes, we don't need this code..
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::AmmoPackCleanUp( void )
|
|
{
|
|
// If we have more than 3 ammo packs out now, destroy the oldest one.
|
|
int iNumPacks = 0;
|
|
CTFAmmoPack *pOldestBox = NULL;
|
|
|
|
// Cycle through all ammobox in the world and remove them
|
|
CBaseEntity *pEnt = gEntList.FindEntityByClassname( NULL, "tf_ammo_pack" );
|
|
while ( pEnt )
|
|
{
|
|
CBaseEntity *pOwner = pEnt->GetOwnerEntity();
|
|
if (pOwner == this)
|
|
{
|
|
CTFAmmoPack *pThisBox = dynamic_cast<CTFAmmoPack *>( pEnt );
|
|
Assert( pThisBox );
|
|
if ( pThisBox )
|
|
{
|
|
iNumPacks++;
|
|
|
|
// Find the oldest one
|
|
if ( pOldestBox == NULL || pOldestBox->GetCreationTime() > pThisBox->GetCreationTime() )
|
|
{
|
|
pOldestBox = pThisBox;
|
|
}
|
|
}
|
|
}
|
|
|
|
pEnt = gEntList.FindEntityByClassname( pEnt, "tf_ammo_pack" );
|
|
}
|
|
|
|
// If they have more than 3 packs active, remove the oldest one
|
|
if ( iNumPacks > 3 && pOldestBox )
|
|
{
|
|
UTIL_Remove( pOldestBox );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::DropAmmoPack( void )
|
|
{
|
|
// We want the ammo packs to look like the player's weapon model they were carrying.
|
|
// except if they are melee or building weapons
|
|
CTFWeaponBase *pWeapon = NULL;
|
|
CTFWeaponBase *pActiveWeapon = m_Shared.GetActiveTFWeapon();
|
|
|
|
if ( !pActiveWeapon || pActiveWeapon->GetTFWpnData().m_bDontDrop )
|
|
{
|
|
// Don't drop this one, find another one to drop
|
|
|
|
int iWeight = -1;
|
|
|
|
// find the highest weighted weapon
|
|
for (int i = 0;i < WeaponCount(); i++)
|
|
{
|
|
CTFWeaponBase *pWpn = ( CTFWeaponBase *)GetWeapon(i);
|
|
if ( !pWpn )
|
|
continue;
|
|
|
|
if ( pWpn->GetTFWpnData().m_bDontDrop )
|
|
continue;
|
|
|
|
int iThisWeight = pWpn->GetTFWpnData().iWeight;
|
|
|
|
if ( iThisWeight > iWeight )
|
|
{
|
|
iWeight = iThisWeight;
|
|
pWeapon = pWpn;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pWeapon = pActiveWeapon;
|
|
}
|
|
|
|
// If we didn't find one, bail
|
|
if ( !pWeapon )
|
|
return;
|
|
|
|
// We need to find bones on the world model, so switch the weapon to it.
|
|
const char *pszWorldModel = pWeapon->GetWorldModel();
|
|
pWeapon->SetModel( pszWorldModel );
|
|
|
|
|
|
// Find the position and angle of the weapons so the "ammo box" matches.
|
|
Vector vecPackOrigin;
|
|
QAngle vecPackAngles;
|
|
if( !CalculateAmmoPackPositionAndAngles( pWeapon, vecPackOrigin, vecPackAngles ) )
|
|
return;
|
|
|
|
// Fill the ammo pack with unused player ammo, if out add a minimum amount.
|
|
int iPrimary = MAX( 5, GetAmmoCount( TF_AMMO_PRIMARY ) );
|
|
int iSecondary = MAX( 5, GetAmmoCount( TF_AMMO_SECONDARY ) );
|
|
int iMetal = MAX( 5, GetAmmoCount( TF_AMMO_METAL ) );
|
|
|
|
// Create the ammo pack.
|
|
CTFAmmoPack *pAmmoPack = CTFAmmoPack::Create( vecPackOrigin, vecPackAngles, this, pszWorldModel );
|
|
Assert( pAmmoPack );
|
|
if ( pAmmoPack )
|
|
{
|
|
// Remove all of the players ammo.
|
|
RemoveAllAmmo();
|
|
|
|
// Fill up the ammo pack.
|
|
pAmmoPack->GiveAmmo( iPrimary, TF_AMMO_PRIMARY );
|
|
pAmmoPack->GiveAmmo( iSecondary, TF_AMMO_SECONDARY );
|
|
pAmmoPack->GiveAmmo( iMetal, TF_AMMO_METAL );
|
|
|
|
Vector vecRight, vecUp;
|
|
AngleVectors( EyeAngles(), NULL, &vecRight, &vecUp );
|
|
|
|
// Calculate the initial impulse on the weapon.
|
|
Vector vecImpulse( 0.0f, 0.0f, 0.0f );
|
|
vecImpulse += vecUp * random->RandomFloat( -0.25, 0.25 );
|
|
vecImpulse += vecRight * random->RandomFloat( -0.25, 0.25 );
|
|
VectorNormalize( vecImpulse );
|
|
vecImpulse *= random->RandomFloat( tf_weapon_ragdoll_velocity_min.GetFloat(), tf_weapon_ragdoll_velocity_max.GetFloat() );
|
|
vecImpulse += GetAbsVelocity();
|
|
|
|
// Cap the impulse.
|
|
float flSpeed = vecImpulse.Length();
|
|
if ( flSpeed > tf_weapon_ragdoll_maxspeed.GetFloat() )
|
|
{
|
|
VectorScale( vecImpulse, tf_weapon_ragdoll_maxspeed.GetFloat() / flSpeed, vecImpulse );
|
|
}
|
|
|
|
if ( pAmmoPack->VPhysicsGetObject() )
|
|
{
|
|
// We can probably remove this when the mass on the weapons is correct!
|
|
pAmmoPack->VPhysicsGetObject()->SetMass( 25.0f );
|
|
AngularImpulse angImpulse( 0, random->RandomFloat( 0, 100 ), 0 );
|
|
pAmmoPack->VPhysicsGetObject()->SetVelocityInstantaneous( &vecImpulse, &angImpulse );
|
|
}
|
|
|
|
pAmmoPack->SetInitialVelocity( vecImpulse );
|
|
|
|
pAmmoPack->m_nSkin = ( GetTeamNumber() == TF_TEAM_RED ) ? 0 : 1;
|
|
|
|
// Give the ammo pack some health, so that trains can destroy it.
|
|
pAmmoPack->SetCollisionGroup( COLLISION_GROUP_DEBRIS );
|
|
pAmmoPack->m_takedamage = DAMAGE_YES;
|
|
pAmmoPack->SetHealth( 900 );
|
|
|
|
pAmmoPack->SetBodygroup( 1, 1 );
|
|
|
|
// Clean up old ammo packs if they exist in the world
|
|
AmmoPackCleanUp();
|
|
}
|
|
pWeapon->SetModel( pWeapon->GetViewModel() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::PlayerDeathThink( void )
|
|
{
|
|
//overridden, do nothing
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Remove the tf items from the player then call into the base class
|
|
// removal of items.
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::RemoveAllItems( bool removeSuit )
|
|
{
|
|
// If the player has a capture flag, drop it.
|
|
if ( HasItem() )
|
|
{
|
|
GetItem()->Drop( this, true );
|
|
|
|
IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_flag_event" );
|
|
if ( event )
|
|
{
|
|
event->SetInt( "player", entindex() );
|
|
event->SetInt( "eventtype", TF_FLAGEVENT_DROPPED );
|
|
event->SetInt( "priority", 8 );
|
|
gameeventmanager->FireEvent( event );
|
|
}
|
|
}
|
|
|
|
if ( m_hOffHandWeapon.Get() )
|
|
{
|
|
HolsterOffHandWeapon();
|
|
|
|
// hide the weapon model
|
|
// don't normally have to do this, unless we have a holster animation
|
|
CBaseViewModel *vm = GetViewModel( 1 );
|
|
if ( vm )
|
|
{
|
|
vm->SetWeaponModel( NULL, NULL );
|
|
}
|
|
|
|
m_hOffHandWeapon = NULL;
|
|
}
|
|
|
|
Weapon_SetLast( NULL );
|
|
UpdateClientData();
|
|
}
|
|
|
|
void CTFPlayer::ClientHearVox( const char *pSentence )
|
|
{
|
|
//TFTODO: implement this.
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::UpdateModel( void )
|
|
{
|
|
SetModel( GetPlayerClass()->GetModelName() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : iSkin -
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::UpdateSkin( int iTeam )
|
|
{
|
|
// The player's skin is team - 2.
|
|
int iSkin = iTeam - 2;
|
|
|
|
// Check to see if the skin actually changed.
|
|
if ( iSkin != m_iLastSkin )
|
|
{
|
|
m_nSkin = iSkin;
|
|
m_iLastSkin = iSkin;
|
|
}
|
|
}
|
|
|
|
//=========================================================================
|
|
// Displays the state of the items specified by the Goal passed in
|
|
void CTFPlayer::DisplayLocalItemStatus( CTFGoal *pGoal )
|
|
{
|
|
#if 0
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
if (pGoal->display_item_status[i] != 0)
|
|
{
|
|
CTFGoalItem *pItem = Finditem(pGoal->display_item_status[i]);
|
|
if (pItem)
|
|
DisplayItemStatus(pGoal, this, pItem);
|
|
else
|
|
ClientPrint( this, HUD_PRINTTALK, "#Item_missing" );
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//=========================================================================
|
|
// Called when the player disconnects from the server.
|
|
void CTFPlayer::TeamFortress_ClientDisconnected( void )
|
|
{
|
|
TeamFortress_RemoveEverythingFromWorld();
|
|
RemoveNemesisRelationships();
|
|
|
|
RemoveAllWeapons();
|
|
}
|
|
|
|
//=========================================================================
|
|
// Removes everything this player has (buildings, grenades, etc.) from the world
|
|
void CTFPlayer::TeamFortress_RemoveEverythingFromWorld( void )
|
|
{
|
|
TeamFortress_RemoveRockets();
|
|
TeamFortress_RemovePipebombs();
|
|
|
|
// Destroy any buildables - this should replace TeamFortress_RemoveBuildings
|
|
RemoveAllObjects();
|
|
}
|
|
|
|
//=========================================================================
|
|
// Removes all rockets the player has fired into the world
|
|
// (this prevents a team kill cheat where players would fire rockets
|
|
// then change teams to kill their own team)
|
|
void CTFPlayer::TeamFortress_RemoveRockets( void )
|
|
{
|
|
RemoveOwnedEnt( "tf_weapon_rocket" );
|
|
RemoveOwnedEnt( "tf_weapon_flamerocket" );
|
|
}
|
|
|
|
//=========================================================================
|
|
// Removes all pipebombs from the world
|
|
void CTFPlayer::TeamFortress_RemovePipebombs( void )
|
|
{
|
|
CTFPlayerClass *pClass = GetPlayerClass();
|
|
if ( pClass && pClass->GetClassIndex() == TF_CLASS_DEMOMAN )
|
|
{
|
|
RemoveOwnedEnt( "tf_projectile_pipe", true );
|
|
}
|
|
}
|
|
|
|
|
|
//=========================================================================
|
|
// Remove all of an ent owned by this player
|
|
void CTFPlayer::RemoveOwnedEnt( char *pEntName, bool bGrenade )
|
|
{
|
|
CBaseEntity *pEnt = gEntList.FindEntityByClassname( NULL, pEntName );
|
|
while ( pEnt )
|
|
{
|
|
// if the player owns this entity, remove it
|
|
bool bOwner = (pEnt->GetOwnerEntity() == this);
|
|
|
|
if ( !bOwner && bGrenade )
|
|
{
|
|
CBaseGrenade *pGrenade = dynamic_cast<CBaseGrenade*>(pEnt);
|
|
Assert( pGrenade );
|
|
if ( pGrenade )
|
|
{
|
|
bOwner = (pGrenade->GetThrower() == this);
|
|
}
|
|
}
|
|
|
|
if ( bOwner )
|
|
{
|
|
pEnt->SetThink( &BaseClass::SUB_Remove );
|
|
pEnt->SetNextThink( gpGlobals->curtime );
|
|
pEnt->SetTouch( NULL );
|
|
pEnt->AddEffects( EF_NODRAW );
|
|
}
|
|
|
|
pEnt = gEntList.FindEntityByClassname( pEnt, pEntName );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::NoteWeaponFired()
|
|
{
|
|
Assert( m_pCurrentCommand );
|
|
if( m_pCurrentCommand )
|
|
{
|
|
m_iLastWeaponFireUsercmd = m_pCurrentCommand->command_number;
|
|
}
|
|
}
|
|
|
|
//=============================================================================
|
|
//
|
|
// Player state functions.
|
|
//
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
CPlayerStateInfo *CTFPlayer::StateLookupInfo( int nState )
|
|
{
|
|
// This table MUST match the
|
|
static CPlayerStateInfo playerStateInfos[] =
|
|
{
|
|
{ TF_STATE_ACTIVE, "TF_STATE_ACTIVE", &CTFPlayer::StateEnterACTIVE, NULL, NULL },
|
|
{ TF_STATE_WELCOME, "TF_STATE_WELCOME", &CTFPlayer::StateEnterWELCOME, NULL, &CTFPlayer::StateThinkWELCOME },
|
|
{ TF_STATE_OBSERVER, "TF_STATE_OBSERVER", &CTFPlayer::StateEnterOBSERVER, NULL, &CTFPlayer::StateThinkOBSERVER },
|
|
{ TF_STATE_DYING, "TF_STATE_DYING", &CTFPlayer::StateEnterDYING, NULL, &CTFPlayer::StateThinkDYING },
|
|
};
|
|
|
|
for ( int iState = 0; iState < ARRAYSIZE( playerStateInfos ); ++iState )
|
|
{
|
|
if ( playerStateInfos[iState].m_nPlayerState == nState )
|
|
return &playerStateInfos[iState];
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StateEnter( int nState )
|
|
{
|
|
m_Shared.m_nPlayerState = nState;
|
|
m_pStateInfo = StateLookupInfo( nState );
|
|
|
|
if ( tf_playerstatetransitions.GetInt() == -1 || tf_playerstatetransitions.GetInt() == entindex() )
|
|
{
|
|
if ( m_pStateInfo )
|
|
Msg( "ShowStateTransitions: entering '%s'\n", m_pStateInfo->m_pStateName );
|
|
else
|
|
Msg( "ShowStateTransitions: entering #%d\n", nState );
|
|
}
|
|
|
|
// Initialize the new state.
|
|
if ( m_pStateInfo && m_pStateInfo->pfnEnterState )
|
|
{
|
|
(this->*m_pStateInfo->pfnEnterState)();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StateLeave( void )
|
|
{
|
|
if ( m_pStateInfo && m_pStateInfo->pfnLeaveState )
|
|
{
|
|
(this->*m_pStateInfo->pfnLeaveState)();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StateTransition( int nState )
|
|
{
|
|
StateLeave();
|
|
StateEnter( nState );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StateEnterWELCOME( void )
|
|
{
|
|
PickWelcomeObserverPoint();
|
|
|
|
StartObserverMode( OBS_MODE_FIXED );
|
|
|
|
// Important to set MOVETYPE_NONE or our physics object will fall while we're sitting at one of the intro cameras.
|
|
SetMoveType( MOVETYPE_NONE );
|
|
AddSolidFlags( FSOLID_NOT_SOLID );
|
|
AddEffects( EF_NODRAW | EF_NOSHADOW );
|
|
|
|
PhysObjectSleep();
|
|
|
|
if ( gpGlobals->eLoadType == MapLoad_Background )
|
|
{
|
|
m_bSeenRoundInfo = true;
|
|
|
|
ChangeTeam( TEAM_SPECTATOR );
|
|
}
|
|
else if ( (TFGameRules() && TFGameRules()->IsLoadingBugBaitReport()) )
|
|
{
|
|
m_bSeenRoundInfo = true;
|
|
|
|
ChangeTeam( TF_TEAM_BLUE );
|
|
SetDesiredPlayerClassIndex( TF_CLASS_SCOUT );
|
|
ForceRespawn();
|
|
}
|
|
else if ( IsInCommentaryMode() )
|
|
{
|
|
m_bSeenRoundInfo = true;
|
|
}
|
|
else
|
|
{
|
|
if ( !IsX360() )
|
|
{
|
|
KeyValues *data = new KeyValues( "data" );
|
|
data->SetString( "title", "#TF_Welcome" ); // info panel title
|
|
data->SetString( "type", "1" ); // show userdata from stringtable entry
|
|
data->SetString( "msg", "motd" ); // use this stringtable entry
|
|
data->SetString( "cmd", "mapinfo" ); // exec this command if panel closed
|
|
|
|
ShowViewPortPanel( PANEL_INFO, true, data );
|
|
|
|
data->deleteThis();
|
|
}
|
|
else
|
|
{
|
|
ShowViewPortPanel( PANEL_MAPINFO, true );
|
|
}
|
|
|
|
m_bSeenRoundInfo = false;
|
|
}
|
|
|
|
m_bIsIdle = false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StateThinkWELCOME( void )
|
|
{
|
|
if ( IsInCommentaryMode() && !IsFakeClient() )
|
|
{
|
|
ChangeTeam( TF_TEAM_BLUE );
|
|
SetDesiredPlayerClassIndex( TF_CLASS_SCOUT );
|
|
ForceRespawn();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StateEnterACTIVE()
|
|
{
|
|
SetMoveType( MOVETYPE_WALK );
|
|
RemoveEffects( EF_NODRAW | EF_NOSHADOW );
|
|
RemoveSolidFlags( FSOLID_NOT_SOLID );
|
|
m_Local.m_iHideHUD = 0;
|
|
PhysObjectWake();
|
|
|
|
m_flLastAction = gpGlobals->curtime;
|
|
m_bIsIdle = false;
|
|
|
|
// If we're a Medic, start thinking to regen myself
|
|
if ( IsPlayerClass( TF_CLASS_MEDIC ) )
|
|
{
|
|
SetContextThink( &CTFPlayer::MedicRegenThink, gpGlobals->curtime + TF_MEDIC_REGEN_TIME, "MedicRegenThink" );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::SetObserverMode(int mode)
|
|
{
|
|
if ( mode < OBS_MODE_NONE || mode >= NUM_OBSERVER_MODES )
|
|
return false;
|
|
|
|
// Skip OBS_MODE_POI as we're not using that.
|
|
// no POI mode in src2013, remove
|
|
//if ( mode == OBS_MODE_POI )
|
|
//{
|
|
// mode++;
|
|
//}
|
|
|
|
// Skip over OBS_MODE_ROAMING for dead players
|
|
if( GetTeamNumber() > TEAM_SPECTATOR )
|
|
{
|
|
if ( IsDead() && ( mode > OBS_MODE_FIXED ) && mp_fadetoblack.GetBool() )
|
|
{
|
|
mode = OBS_MODE_CHASE;
|
|
}
|
|
else if ( mode == OBS_MODE_ROAMING )
|
|
{
|
|
mode = OBS_MODE_IN_EYE;
|
|
}
|
|
}
|
|
|
|
if ( m_iObserverMode > OBS_MODE_DEATHCAM )
|
|
{
|
|
// remember mode if we were really spectating before
|
|
m_iObserverLastMode = m_iObserverMode;
|
|
}
|
|
|
|
m_iObserverMode = mode;
|
|
m_flLastAction = gpGlobals->curtime;
|
|
|
|
switch ( mode )
|
|
{
|
|
case OBS_MODE_NONE:
|
|
case OBS_MODE_FIXED :
|
|
case OBS_MODE_DEATHCAM :
|
|
SetFOV( this, 0 ); // Reset FOV
|
|
SetViewOffset( vec3_origin );
|
|
SetMoveType( MOVETYPE_NONE );
|
|
break;
|
|
|
|
case OBS_MODE_CHASE :
|
|
case OBS_MODE_IN_EYE :
|
|
// udpate FOV and viewmodels
|
|
SetObserverTarget( m_hObserverTarget );
|
|
SetMoveType( MOVETYPE_OBSERVER );
|
|
break;
|
|
|
|
case OBS_MODE_ROAMING :
|
|
SetFOV( this, 0 ); // Reset FOV
|
|
SetObserverTarget( m_hObserverTarget );
|
|
SetViewOffset( vec3_origin );
|
|
SetMoveType( MOVETYPE_OBSERVER );
|
|
break;
|
|
|
|
case OBS_MODE_FREEZECAM:
|
|
SetFOV( this, 0 ); // Reset FOV
|
|
SetObserverTarget( m_hObserverTarget );
|
|
SetViewOffset( vec3_origin );
|
|
SetMoveType( MOVETYPE_OBSERVER );
|
|
break;
|
|
}
|
|
|
|
CheckObserverSettings();
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StateEnterOBSERVER( void )
|
|
{
|
|
// Always start a spectator session in chase mode
|
|
m_iObserverLastMode = OBS_MODE_CHASE;
|
|
|
|
if( m_hObserverTarget == NULL )
|
|
{
|
|
// find a new observer target
|
|
CheckObserverSettings();
|
|
}
|
|
|
|
if ( !m_bAbortFreezeCam )
|
|
{
|
|
FindInitialObserverTarget();
|
|
}
|
|
|
|
StartObserverMode( m_iObserverLastMode );
|
|
|
|
PhysObjectSleep();
|
|
|
|
m_bIsIdle = false;
|
|
|
|
if ( GetTeamNumber() != TEAM_SPECTATOR )
|
|
{
|
|
HandleFadeToBlack();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StateThinkOBSERVER()
|
|
{
|
|
// Make sure nobody has changed any of our state.
|
|
Assert( m_takedamage == DAMAGE_NO );
|
|
Assert( IsSolidFlagSet( FSOLID_NOT_SOLID ) );
|
|
|
|
// Must be dead.
|
|
Assert( m_lifeState == LIFE_DEAD );
|
|
Assert( pl.deadflag );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StateEnterDYING( void )
|
|
{
|
|
SetMoveType( MOVETYPE_NONE );
|
|
AddSolidFlags( FSOLID_NOT_SOLID );
|
|
|
|
m_bPlayedFreezeCamSound = false;
|
|
m_bAbortFreezeCam = false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Move the player to observer mode once the dying process is over
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StateThinkDYING( void )
|
|
{
|
|
// If we have a ragdoll, it's time to go to deathcam
|
|
if ( !m_bAbortFreezeCam && m_hRagdoll &&
|
|
(m_lifeState == LIFE_DYING || m_lifeState == LIFE_DEAD) &&
|
|
GetObserverMode() != OBS_MODE_FREEZECAM )
|
|
{
|
|
if ( GetObserverMode() != OBS_MODE_DEATHCAM )
|
|
{
|
|
StartObserverMode( OBS_MODE_DEATHCAM ); // go to observer mode
|
|
}
|
|
RemoveEffects( EF_NODRAW | EF_NOSHADOW ); // still draw player body
|
|
}
|
|
|
|
float flTimeInFreeze = spec_freeze_traveltime.GetFloat() + spec_freeze_time.GetFloat();
|
|
float flFreezeEnd = (m_flDeathTime + TF_DEATH_ANIMATION_TIME + flTimeInFreeze );
|
|
if ( !m_bPlayedFreezeCamSound && GetObserverTarget() && GetObserverTarget() != this )
|
|
{
|
|
// Start the sound so that it ends at the freezecam lock on time
|
|
float flFreezeSoundLength = 0.3;
|
|
float flFreezeSoundTime = (m_flDeathTime + TF_DEATH_ANIMATION_TIME ) + spec_freeze_traveltime.GetFloat() - flFreezeSoundLength;
|
|
if ( gpGlobals->curtime >= flFreezeSoundTime )
|
|
{
|
|
CSingleUserRecipientFilter filter( this );
|
|
EmitSound_t params;
|
|
params.m_flSoundTime = 0;
|
|
params.m_pSoundName = "TFPlayer.FreezeCam";
|
|
EmitSound( filter, entindex(), params );
|
|
|
|
m_bPlayedFreezeCamSound = true;
|
|
}
|
|
}
|
|
|
|
if ( gpGlobals->curtime >= (m_flDeathTime + TF_DEATH_ANIMATION_TIME ) ) // allow x seconds death animation / death cam
|
|
{
|
|
if ( GetObserverTarget() && GetObserverTarget() != this )
|
|
{
|
|
if ( !m_bAbortFreezeCam && gpGlobals->curtime < flFreezeEnd )
|
|
{
|
|
if ( GetObserverMode() != OBS_MODE_FREEZECAM )
|
|
{
|
|
StartObserverMode( OBS_MODE_FREEZECAM );
|
|
PhysObjectSleep();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( GetObserverMode() == OBS_MODE_FREEZECAM )
|
|
{
|
|
// If we're in freezecam, and we want out, abort. (only if server is not using mp_fadetoblack)
|
|
if ( m_bAbortFreezeCam && !mp_fadetoblack.GetBool() )
|
|
{
|
|
if ( m_hObserverTarget == NULL )
|
|
{
|
|
// find a new observer target
|
|
CheckObserverSettings();
|
|
}
|
|
|
|
FindInitialObserverTarget();
|
|
SetObserverMode( OBS_MODE_CHASE );
|
|
ShowViewPortPanel( "specgui" , ModeWantsSpectatorGUI(OBS_MODE_CHASE) );
|
|
}
|
|
}
|
|
|
|
// Don't allow anyone to respawn until freeze time is over, even if they're not
|
|
// in freezecam. This prevents players skipping freezecam to spawn faster.
|
|
if ( gpGlobals->curtime < flFreezeEnd )
|
|
return;
|
|
|
|
m_lifeState = LIFE_RESPAWNABLE;
|
|
|
|
StopAnimation();
|
|
|
|
AddEffects( EF_NOINTERP );
|
|
|
|
if ( GetMoveType() != MOVETYPE_NONE && (GetFlags() & FL_ONGROUND) )
|
|
SetMoveType( MOVETYPE_NONE );
|
|
|
|
StateTransition( TF_STATE_OBSERVER );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::AttemptToExitFreezeCam( void )
|
|
{
|
|
float flFreezeTravelTime = (m_flDeathTime + TF_DEATH_ANIMATION_TIME ) + spec_freeze_traveltime.GetFloat() + 0.5;
|
|
if ( gpGlobals->curtime < flFreezeTravelTime )
|
|
return;
|
|
|
|
m_bAbortFreezeCam = true;
|
|
}
|
|
|
|
class CIntroViewpoint : public CPointEntity
|
|
{
|
|
DECLARE_CLASS( CIntroViewpoint, CPointEntity );
|
|
public:
|
|
DECLARE_DATADESC();
|
|
|
|
virtual int UpdateTransmitState()
|
|
{
|
|
return SetTransmitState( FL_EDICT_ALWAYS );
|
|
}
|
|
|
|
int m_iIntroStep;
|
|
float m_flStepDelay;
|
|
string_t m_iszMessage;
|
|
string_t m_iszGameEvent;
|
|
float m_flEventDelay;
|
|
int m_iGameEventData;
|
|
float m_flFOV;
|
|
};
|
|
|
|
BEGIN_DATADESC( CIntroViewpoint )
|
|
DEFINE_KEYFIELD( m_iIntroStep, FIELD_INTEGER, "step_number" ),
|
|
DEFINE_KEYFIELD( m_flStepDelay, FIELD_FLOAT, "time_delay" ),
|
|
DEFINE_KEYFIELD( m_iszMessage, FIELD_STRING, "hint_message" ),
|
|
DEFINE_KEYFIELD( m_iszGameEvent, FIELD_STRING, "event_to_fire" ),
|
|
DEFINE_KEYFIELD( m_flEventDelay, FIELD_FLOAT, "event_delay" ),
|
|
DEFINE_KEYFIELD( m_iGameEventData, FIELD_INTEGER, "event_data_int" ),
|
|
DEFINE_KEYFIELD( m_flFOV, FIELD_FLOAT, "fov" ),
|
|
END_DATADESC()
|
|
|
|
LINK_ENTITY_TO_CLASS( game_intro_viewpoint, CIntroViewpoint );
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Give the player some ammo.
|
|
// Input : iCount - Amount of ammo to give.
|
|
// iAmmoIndex - Index of the ammo into the AmmoInfoArray
|
|
// iMax - Max carrying capability of the player
|
|
// Output : Amount of ammo actually given
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPlayer::GiveAmmo( int iCount, int iAmmoIndex, bool bSuppressSound )
|
|
{
|
|
if ( iCount <= 0 )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if ( !g_pGameRules->CanHaveAmmo( this, iAmmoIndex ) )
|
|
{
|
|
// game rules say I can't have any more of this ammo type.
|
|
return 0;
|
|
}
|
|
|
|
if ( iAmmoIndex < 0 || iAmmoIndex >= MAX_AMMO_SLOTS )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
int iMax = m_PlayerClass.GetData()->m_aAmmoMax[iAmmoIndex];
|
|
int iAdd = MIN( iCount, iMax - GetAmmoCount(iAmmoIndex) );
|
|
if ( iAdd < 1 )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Ammo pickup sound
|
|
if ( !bSuppressSound )
|
|
{
|
|
EmitSound( "BaseCombatCharacter.AmmoPickup" );
|
|
}
|
|
|
|
CBaseCombatCharacter::GiveAmmo( iAdd, iAmmoIndex );
|
|
return iAdd;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Reset player's information and force him to spawn
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::ForceRespawn( void )
|
|
{
|
|
CTF_GameStats.Event_PlayerForceRespawn( this );
|
|
|
|
m_flSpawnTime = gpGlobals->curtime;
|
|
|
|
int iDesiredClass = GetDesiredPlayerClassIndex();
|
|
|
|
if ( iDesiredClass == TF_CLASS_UNDEFINED )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( iDesiredClass == TF_CLASS_RANDOM )
|
|
{
|
|
// Don't let them be the same class twice in a row
|
|
do{
|
|
iDesiredClass = random->RandomInt( TF_FIRST_NORMAL_CLASS, TF_LAST_NORMAL_CLASS );
|
|
} while( iDesiredClass == GetPlayerClass()->GetClassIndex() );
|
|
}
|
|
|
|
if ( HasTheFlag() )
|
|
{
|
|
DropFlag();
|
|
}
|
|
|
|
if ( GetPlayerClass()->GetClassIndex() != iDesiredClass )
|
|
{
|
|
// clean up any pipebombs/buildings in the world (no explosions)
|
|
TeamFortress_RemoveEverythingFromWorld();
|
|
|
|
GetPlayerClass()->Init( iDesiredClass );
|
|
|
|
CTF_GameStats.Event_PlayerChangedClass( this );
|
|
}
|
|
|
|
m_Shared.RemoveAllCond( NULL );
|
|
|
|
RemoveAllItems( true );
|
|
|
|
// Reset ground state for airwalk animations
|
|
SetGroundEntity( NULL );
|
|
|
|
// TODO: move this into conditions
|
|
RemoveTeleportEffect();
|
|
|
|
// remove invisibility very quickly
|
|
m_Shared.FadeInvis( 0.1 );
|
|
|
|
// Stop any firing that was taking place before respawn.
|
|
m_nButtons = 0;
|
|
|
|
StateTransition( TF_STATE_ACTIVE );
|
|
Spawn();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Do nothing multiplayer_animstate takes care of animation.
|
|
// Input : playerAnim -
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::SetAnimation( PLAYER_ANIM playerAnim )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Handle cheat commands
|
|
// Input : iImpulse -
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::CheatImpulseCommands( int iImpulse )
|
|
{
|
|
switch( iImpulse )
|
|
{
|
|
case 101:
|
|
{
|
|
if( sv_cheats->GetBool() )
|
|
{
|
|
extern int gEvilImpulse101;
|
|
gEvilImpulse101 = true;
|
|
|
|
GiveAmmo( 1000, TF_AMMO_PRIMARY );
|
|
GiveAmmo( 1000, TF_AMMO_SECONDARY );
|
|
GiveAmmo( 1000, TF_AMMO_METAL );
|
|
TakeHealth( 999, DMG_GENERIC );
|
|
|
|
gEvilImpulse101 = false;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
{
|
|
BaseClass::CheatImpulseCommands( iImpulse );
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::SetWeaponBuilder( CTFWeaponBuilder *pBuilder )
|
|
{
|
|
m_hWeaponBuilder = pBuilder;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
CTFWeaponBuilder *CTFPlayer::GetWeaponBuilder( void )
|
|
{
|
|
Assert( 0 );
|
|
return m_hWeaponBuilder;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Returns true if this player is building something
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::IsBuilding( void )
|
|
{
|
|
/*
|
|
CTFWeaponBuilder *pBuilder = GetWeaponBuilder();
|
|
if ( pBuilder )
|
|
return pBuilder->IsBuilding();
|
|
*/
|
|
|
|
return false;
|
|
}
|
|
|
|
void CTFPlayer::RemoveBuildResources( int iAmount )
|
|
{
|
|
RemoveAmmo( iAmount, TF_AMMO_METAL );
|
|
}
|
|
|
|
void CTFPlayer::AddBuildResources( int iAmount )
|
|
{
|
|
GiveAmmo( iAmount, TF_AMMO_METAL );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
CBaseObject *CTFPlayer::GetObject( int index )
|
|
{
|
|
return (CBaseObject *)( m_aObjects[index].Get() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPlayer::GetObjectCount( void )
|
|
{
|
|
return m_aObjects.Count();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Remove all the player's objects
|
|
// If bForceAll is set, remove all of them immediately.
|
|
// Otherwise, make them all deteriorate over time.
|
|
// If iClass is passed in, don't remove any objects that can be built
|
|
// by that class. If bReturnResources is set, the cost of any destroyed
|
|
// objects will be returned to the player.
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::RemoveAllObjects( void )
|
|
{
|
|
// Remove all the player's objects
|
|
for (int i = GetObjectCount()-1; i >= 0; i--)
|
|
{
|
|
CBaseObject *obj = GetObject(i);
|
|
Assert( obj );
|
|
|
|
if ( obj )
|
|
{
|
|
UTIL_Remove( obj );
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StopPlacement( void )
|
|
{
|
|
/*
|
|
// Tell our builder weapon
|
|
CTFWeaponBuilder *pBuilder = GetWeaponBuilder();
|
|
if ( pBuilder )
|
|
{
|
|
pBuilder->StopPlacement();
|
|
}
|
|
*/
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Player has started building an object
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPlayer::StartedBuildingObject( int iObjectType )
|
|
{
|
|
// Deduct the cost of the object
|
|
int iCost = CalculateObjectCost( iObjectType );
|
|
if ( iCost > GetBuildResources() )
|
|
{
|
|
// Player must have lost resources since he started placing
|
|
return 0;
|
|
}
|
|
|
|
RemoveBuildResources( iCost );
|
|
|
|
// If the object costs 0, we need to return non-0 to mean success
|
|
if ( !iCost )
|
|
return 1;
|
|
|
|
return iCost;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Player has aborted building something
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::StoppedBuilding( int iObjectType )
|
|
{
|
|
/*
|
|
int iCost = CalculateObjectCost( iObjectType );
|
|
|
|
AddBuildResources( iCost );
|
|
|
|
// Tell our builder weapon
|
|
CTFWeaponBuilder *pBuilder = GetWeaponBuilder();
|
|
if ( pBuilder )
|
|
{
|
|
pBuilder->StoppedBuilding( iObjectType );
|
|
}
|
|
*/
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Object has been built by this player
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::FinishedObject( CBaseObject *pObject )
|
|
{
|
|
AddObject( pObject );
|
|
CTF_GameStats.Event_PlayerCreatedBuilding( this, pObject );
|
|
|
|
/*
|
|
// Tell our builder weapon
|
|
CTFWeaponBuilder *pBuilder = GetWeaponBuilder();
|
|
if ( pBuilder )
|
|
{
|
|
pBuilder->FinishedObject();
|
|
}
|
|
*/
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Add the specified object to this player's object list.
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::AddObject( CBaseObject *pObject )
|
|
{
|
|
TRACE_OBJECT( UTIL_VarArgs( "%0.2f CBaseTFPlayer::AddObject adding object %p:%s to player %s\n", gpGlobals->curtime, pObject, pObject->GetClassname(), GetPlayerName() ) );
|
|
|
|
// Make a handle out of it
|
|
CHandle<CBaseObject> hObject;
|
|
hObject = pObject;
|
|
|
|
bool alreadyInList = PlayerOwnsObject( pObject );
|
|
Assert( !alreadyInList );
|
|
if ( !alreadyInList )
|
|
{
|
|
m_aObjects.AddToTail( hObject );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Object built by this player has been destroyed
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::OwnedObjectDestroyed( CBaseObject *pObject )
|
|
{
|
|
TRACE_OBJECT( UTIL_VarArgs( "%0.2f CBaseTFPlayer::OwnedObjectDestroyed player %s object %p:%s\n", gpGlobals->curtime,
|
|
GetPlayerName(),
|
|
pObject,
|
|
pObject->GetClassname() ) );
|
|
|
|
RemoveObject( pObject );
|
|
|
|
// Tell our builder weapon so it recalculates the state of the build icons
|
|
/*
|
|
CTFWeaponBuilder *pBuilder = GetWeaponBuilder();
|
|
if ( pBuilder )
|
|
{
|
|
pBuilder->RecalcState();
|
|
}
|
|
*/
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Removes an object from the player
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::RemoveObject( CBaseObject *pObject )
|
|
{
|
|
TRACE_OBJECT( UTIL_VarArgs( "%0.2f CBaseTFPlayer::RemoveObject %p:%s from player %s\n", gpGlobals->curtime,
|
|
pObject,
|
|
pObject->GetClassname(),
|
|
GetPlayerName() ) );
|
|
|
|
Assert( pObject );
|
|
|
|
int i;
|
|
for ( i = m_aObjects.Count(); --i >= 0; )
|
|
{
|
|
// Also, while we're at it, remove all other bogus ones too...
|
|
if ( (!m_aObjects[i].Get()) || (m_aObjects[i] == pObject))
|
|
{
|
|
m_aObjects.FastRemove(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// See if the player owns this object
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::PlayerOwnsObject( CBaseObject *pObject )
|
|
{
|
|
return ( m_aObjects.Find( pObject ) != -1 );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::PlayFlinch( const CTakeDamageInfo &info )
|
|
{
|
|
// Don't play flinches if we just died.
|
|
if ( !IsAlive() )
|
|
return;
|
|
|
|
// No pain flinches while disguised, our man has supreme discipline
|
|
if ( m_Shared.InCond( TF_COND_DISGUISED ) )
|
|
return;
|
|
|
|
PlayerAnimEvent_t flinchEvent;
|
|
|
|
switch ( LastHitGroup() )
|
|
{
|
|
// pick a region-specific flinch
|
|
case HITGROUP_HEAD:
|
|
flinchEvent = PLAYERANIMEVENT_FLINCH_HEAD;
|
|
break;
|
|
case HITGROUP_LEFTARM:
|
|
flinchEvent = PLAYERANIMEVENT_FLINCH_LEFTARM;
|
|
break;
|
|
case HITGROUP_RIGHTARM:
|
|
flinchEvent = PLAYERANIMEVENT_FLINCH_RIGHTARM;
|
|
break;
|
|
case HITGROUP_LEFTLEG:
|
|
flinchEvent = PLAYERANIMEVENT_FLINCH_LEFTLEG;
|
|
break;
|
|
case HITGROUP_RIGHTLEG:
|
|
flinchEvent = PLAYERANIMEVENT_FLINCH_RIGHTLEG;
|
|
break;
|
|
case HITGROUP_STOMACH:
|
|
case HITGROUP_CHEST:
|
|
case HITGROUP_GEAR:
|
|
case HITGROUP_GENERIC:
|
|
default:
|
|
// just get a generic flinch.
|
|
flinchEvent = PLAYERANIMEVENT_FLINCH_CHEST;
|
|
break;
|
|
}
|
|
|
|
DoAnimationEvent( flinchEvent );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Plays the crit sound that players that get crit hear
|
|
//-----------------------------------------------------------------------------
|
|
float CTFPlayer::PlayCritReceivedSound( void )
|
|
{
|
|
float flCritPainLength = 0;
|
|
// Play a custom pain sound to the guy taking the damage
|
|
CSingleUserRecipientFilter receiverfilter( this );
|
|
EmitSound_t params;
|
|
params.m_flSoundTime = 0;
|
|
params.m_pSoundName = "TFPlayer.CritPain";
|
|
params.m_pflSoundDuration = &flCritPainLength;
|
|
EmitSound( receiverfilter, entindex(), params );
|
|
|
|
return flCritPainLength;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::PainSound( const CTakeDamageInfo &info )
|
|
{
|
|
// Don't make sounds if we just died. DeathSound will handle that.
|
|
if ( !IsAlive() )
|
|
return;
|
|
|
|
// no pain sounds while disguised, our man has supreme discipline
|
|
if ( m_Shared.InCond( TF_COND_DISGUISED ) )
|
|
return;
|
|
|
|
if ( m_flNextPainSoundTime > gpGlobals->curtime )
|
|
return;
|
|
|
|
// Don't play falling pain sounds, they have their own system
|
|
if ( info.GetDamageType() & DMG_FALL )
|
|
return;
|
|
|
|
if ( info.GetDamageType() & DMG_DROWN )
|
|
{
|
|
EmitSound( "TFPlayer.Drown" );
|
|
return;
|
|
}
|
|
|
|
if ( info.GetDamageType() & DMG_BURN )
|
|
{
|
|
// Looping fire pain sound is done in CTFPlayerShared::ConditionThink
|
|
return;
|
|
}
|
|
|
|
float flPainLength = 0;
|
|
|
|
bool bAttackerIsPlayer = ( info.GetAttacker() && info.GetAttacker()->IsPlayer() );
|
|
|
|
CMultiplayer_Expresser *pExpresser = GetMultiplayerExpresser();
|
|
Assert( pExpresser );
|
|
|
|
pExpresser->AllowMultipleScenes();
|
|
|
|
// speak a pain concept here, send to everyone but the attacker
|
|
CPASFilter filter( GetAbsOrigin() );
|
|
|
|
if ( bAttackerIsPlayer )
|
|
{
|
|
filter.RemoveRecipient( ToBasePlayer( info.GetAttacker() ) );
|
|
}
|
|
|
|
// play a crit sound to the victim ( us )
|
|
if ( info.GetDamageType() & DMG_CRITICAL )
|
|
{
|
|
flPainLength = PlayCritReceivedSound();
|
|
|
|
// remove us from hearing our own pain sound if we hear the crit sound
|
|
filter.RemoveRecipient( this );
|
|
}
|
|
|
|
char szResponse[AI_Response::MAX_RESPONSE_NAME];
|
|
|
|
if ( SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_PAIN, "damagecritical:1", szResponse, AI_Response::MAX_RESPONSE_NAME, &filter ) )
|
|
{
|
|
flPainLength = MAX( GetSceneDuration( szResponse ), flPainLength );
|
|
}
|
|
|
|
// speak a louder pain concept to just the attacker
|
|
if ( bAttackerIsPlayer )
|
|
{
|
|
CSingleUserRecipientFilter attackerFilter( ToBasePlayer( info.GetAttacker() ) );
|
|
SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_ATTACKER_PAIN, "damagecritical:1", szResponse, AI_Response::MAX_RESPONSE_NAME, &attackerFilter );
|
|
}
|
|
|
|
pExpresser->DisallowMultipleScenes();
|
|
|
|
m_flNextPainSoundTime = gpGlobals->curtime + flPainLength;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::DeathSound( const CTakeDamageInfo &info )
|
|
{
|
|
// Don't make death sounds when choosing a class
|
|
if ( IsPlayerClass( TF_CLASS_UNDEFINED ) )
|
|
return;
|
|
|
|
TFPlayerClassData_t *pData = GetPlayerClass()->GetData();
|
|
if ( !pData )
|
|
return;
|
|
|
|
if ( m_LastDamageType & DMG_FALL ) // Did we die from falling?
|
|
{
|
|
// They died in the fall. Play a splat sound.
|
|
EmitSound( "Player.FallGib" );
|
|
}
|
|
else if ( m_LastDamageType & DMG_BLAST )
|
|
{
|
|
EmitSound( pData->m_szExplosionDeathSound );
|
|
}
|
|
else if ( m_LastDamageType & DMG_CRITICAL )
|
|
{
|
|
EmitSound( pData->m_szCritDeathSound );
|
|
|
|
PlayCritReceivedSound();
|
|
}
|
|
else if ( m_LastDamageType & DMG_CLUB )
|
|
{
|
|
EmitSound( pData->m_szMeleeDeathSound );
|
|
}
|
|
else
|
|
{
|
|
EmitSound( pData->m_szDeathSound );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: called when this player burns another player
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::OnBurnOther( CTFPlayer *pTFPlayerVictim )
|
|
{
|
|
#define ACHIEVEMENT_BURN_TIME_WINDOW 30.0f
|
|
#define ACHIEVEMENT_BURN_VICTIMS 5
|
|
// add current time we burned another player to head of vector
|
|
m_aBurnOtherTimes.AddToHead( gpGlobals->curtime );
|
|
|
|
// remove any burn times that are older than the burn window from the list
|
|
float flTimeDiscard = gpGlobals->curtime - ACHIEVEMENT_BURN_TIME_WINDOW;
|
|
for ( int i = 1; i < m_aBurnOtherTimes.Count(); i++ )
|
|
{
|
|
if ( m_aBurnOtherTimes[i] < flTimeDiscard )
|
|
{
|
|
m_aBurnOtherTimes.RemoveMultiple( i, m_aBurnOtherTimes.Count() - i );
|
|
break;
|
|
}
|
|
}
|
|
|
|
// see if we've burned enough players in time window to satisfy achievement
|
|
if ( m_aBurnOtherTimes.Count() >= ACHIEVEMENT_BURN_VICTIMS )
|
|
{
|
|
CSingleUserRecipientFilter filter( this );
|
|
UserMessageBegin( filter, "AchievementEvent" );
|
|
WRITE_BYTE( ACHIEVEMENT_TF_BURN_PLAYERSINMINIMIMTIME );
|
|
MessageEnd();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
CTFTeam *CTFPlayer::GetTFTeam( void )
|
|
{
|
|
CTFTeam *pTeam = dynamic_cast<CTFTeam *>( GetTeam() );
|
|
Assert( pTeam );
|
|
return pTeam;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
CTFTeam *CTFPlayer::GetOpposingTFTeam( void )
|
|
{
|
|
int iTeam = GetTeamNumber();
|
|
if ( iTeam == TF_TEAM_RED )
|
|
{
|
|
return TFTeamMgr()->GetTeam( TF_TEAM_BLUE );
|
|
}
|
|
else
|
|
{
|
|
return TFTeamMgr()->GetTeam( TF_TEAM_RED );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Give this player the "i just teleported" effect for 12 seconds
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::TeleportEffect( void )
|
|
{
|
|
m_Shared.AddCond( TF_COND_TELEPORTED );
|
|
|
|
// Also removed on death
|
|
SetContextThink( &CTFPlayer::RemoveTeleportEffect, gpGlobals->curtime + 12, "TFPlayer_TeleportEffect" );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Remove the teleporter effect
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::RemoveTeleportEffect( void )
|
|
{
|
|
m_Shared.RemoveCond( TF_COND_TELEPORTED );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::CreateRagdollEntity( void )
|
|
{
|
|
CreateRagdollEntity( false, false );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Create a ragdoll entity to pass to the client.
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::CreateRagdollEntity( bool bGib, bool bBurning )
|
|
{
|
|
// If we already have a ragdoll destroy it.
|
|
CTFRagdoll *pRagdoll = dynamic_cast<CTFRagdoll*>( m_hRagdoll.Get() );
|
|
if( pRagdoll )
|
|
{
|
|
UTIL_Remove( pRagdoll );
|
|
pRagdoll = NULL;
|
|
}
|
|
Assert( pRagdoll == NULL );
|
|
|
|
// Create a ragdoll.
|
|
pRagdoll = dynamic_cast<CTFRagdoll*>( CreateEntityByName( "tf_ragdoll" ) );
|
|
if ( pRagdoll )
|
|
{
|
|
pRagdoll->m_vecRagdollOrigin = GetAbsOrigin();
|
|
pRagdoll->m_vecRagdollVelocity = GetAbsVelocity();
|
|
pRagdoll->m_vecForce = m_vecTotalBulletForce;
|
|
pRagdoll->m_nForceBone = m_nForceBone;
|
|
Assert( entindex() >= 1 && entindex() <= MAX_PLAYERS );
|
|
pRagdoll->m_iPlayerIndex.Set( entindex() );
|
|
pRagdoll->m_bGib = bGib;
|
|
pRagdoll->m_bBurning = bBurning;
|
|
pRagdoll->m_iTeam = GetTeamNumber();
|
|
pRagdoll->m_iClass = GetPlayerClass()->GetClassIndex();
|
|
}
|
|
|
|
// Turn off the player.
|
|
AddSolidFlags( FSOLID_NOT_SOLID );
|
|
AddEffects( EF_NODRAW | EF_NOSHADOW );
|
|
SetMoveType( MOVETYPE_NONE );
|
|
|
|
// Add additional gib setup.
|
|
if ( bGib )
|
|
{
|
|
m_nRenderFX = kRenderFxRagdoll;
|
|
}
|
|
|
|
// Save ragdoll handle.
|
|
m_hRagdoll = pRagdoll;
|
|
}
|
|
|
|
// Purpose: Destroy's a ragdoll, called with a player is disconnecting.
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::DestroyRagdoll( void )
|
|
{
|
|
CTFRagdoll *pRagdoll = dynamic_cast<CTFRagdoll*>( m_hRagdoll.Get() );
|
|
if( pRagdoll )
|
|
{
|
|
UTIL_Remove( pRagdoll );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::Weapon_FrameUpdate( void )
|
|
{
|
|
BaseClass::Weapon_FrameUpdate();
|
|
|
|
if ( m_hOffHandWeapon.Get() && m_hOffHandWeapon->IsWeaponVisible() )
|
|
{
|
|
m_hOffHandWeapon->Operator_FrameUpdate( this );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::Weapon_HandleAnimEvent( animevent_t *pEvent )
|
|
{
|
|
BaseClass::Weapon_HandleAnimEvent( pEvent );
|
|
|
|
if ( m_hOffHandWeapon.Get() )
|
|
{
|
|
m_hOffHandWeapon->Operator_HandleAnimEvent( pEvent, this );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::Weapon_Drop( CBaseCombatWeapon *pWeapon, const Vector *pvecTarget , const Vector *pVelocity )
|
|
{
|
|
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Remove invisibility, called when player attacks
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::RemoveInvisibility( void )
|
|
{
|
|
if ( !m_Shared.InCond( TF_COND_STEALTHED ) )
|
|
return;
|
|
|
|
// remove quickly
|
|
m_Shared.FadeInvis( 0.5 );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Remove disguise
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::RemoveDisguise( void )
|
|
{
|
|
// remove quickly
|
|
if ( m_Shared.InCond( TF_COND_DISGUISED ) || m_Shared.InCond( TF_COND_DISGUISING ) )
|
|
{
|
|
m_Shared.RemoveDisguise();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::SaveMe( void )
|
|
{
|
|
if ( !IsAlive() || IsPlayerClass( TF_CLASS_UNDEFINED ) || GetTeamNumber() < TF_TEAM_RED )
|
|
return;
|
|
|
|
m_bSaveMeParity = !m_bSaveMeParity;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: drops the flag
|
|
//-----------------------------------------------------------------------------
|
|
void CC_DropItem( void )
|
|
{
|
|
CTFPlayer *pPlayer = ToTFPlayer( UTIL_GetCommandClient() );
|
|
if ( pPlayer )
|
|
{
|
|
pPlayer->DropFlag();
|
|
}
|
|
}
|
|
static ConCommand dropitem( "dropitem", CC_DropItem, "Drop the flag." );
|
|
|
|
class CObserverPoint : public CPointEntity
|
|
{
|
|
DECLARE_CLASS( CObserverPoint, CPointEntity );
|
|
public:
|
|
DECLARE_DATADESC();
|
|
|
|
virtual void Activate( void )
|
|
{
|
|
BaseClass::Activate();
|
|
|
|
if ( m_iszAssociateTeamEntityName != NULL_STRING )
|
|
{
|
|
m_hAssociatedTeamEntity = gEntList.FindEntityByName( NULL, m_iszAssociateTeamEntityName );
|
|
if ( !m_hAssociatedTeamEntity )
|
|
{
|
|
Warning("info_observer_point (%s) couldn't find associated team entity named '%s'\n", GetDebugName(), STRING(m_iszAssociateTeamEntityName) );
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CanUseObserverPoint( CTFPlayer *pPlayer )
|
|
{
|
|
if ( m_bDisabled )
|
|
return false;
|
|
|
|
if ( m_hAssociatedTeamEntity && ( mp_forcecamera.GetInt() == OBS_ALLOW_TEAM ) )
|
|
{
|
|
// If we don't own the associated team entity, we can't use this point
|
|
if ( m_hAssociatedTeamEntity->GetTeamNumber() != pPlayer->GetTeamNumber() && pPlayer->GetTeamNumber() >= FIRST_GAME_TEAM )
|
|
return false;
|
|
}
|
|
|
|
// Only spectate observer points on control points in the current miniround
|
|
if ( g_pObjectiveResource->PlayingMiniRounds() && m_hAssociatedTeamEntity )
|
|
{
|
|
CTeamControlPoint *pPoint = dynamic_cast<CTeamControlPoint*>(m_hAssociatedTeamEntity.Get());
|
|
if ( pPoint )
|
|
{
|
|
bool bInRound = g_pObjectiveResource->IsInMiniRound( pPoint->GetPointIndex() );
|
|
if ( !bInRound )
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
virtual int UpdateTransmitState()
|
|
{
|
|
return SetTransmitState( FL_EDICT_ALWAYS );
|
|
}
|
|
|
|
void InputEnable( inputdata_t &inputdata )
|
|
{
|
|
m_bDisabled = false;
|
|
}
|
|
void InputDisable( inputdata_t &inputdata )
|
|
{
|
|
m_bDisabled = true;
|
|
}
|
|
bool IsDefaultWelcome( void ) { return m_bDefaultWelcome; }
|
|
|
|
public:
|
|
bool m_bDisabled;
|
|
bool m_bDefaultWelcome;
|
|
EHANDLE m_hAssociatedTeamEntity;
|
|
string_t m_iszAssociateTeamEntityName;
|
|
float m_flFOV;
|
|
};
|
|
|
|
BEGIN_DATADESC( CObserverPoint )
|
|
DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ),
|
|
DEFINE_KEYFIELD( m_bDefaultWelcome, FIELD_BOOLEAN, "defaultwelcome" ),
|
|
DEFINE_KEYFIELD( m_iszAssociateTeamEntityName, FIELD_STRING, "associated_team_entity" ),
|
|
DEFINE_KEYFIELD( m_flFOV, FIELD_FLOAT, "fov" ),
|
|
|
|
DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ),
|
|
DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ),
|
|
END_DATADESC()
|
|
|
|
LINK_ENTITY_TO_CLASS(info_observer_point,CObserverPoint);
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Builds a list of entities that this player can observe.
|
|
// Returns the index into the list of the player's current observer target.
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPlayer::BuildObservableEntityList( void )
|
|
{
|
|
m_hObservableEntities.Purge();
|
|
int iCurrentIndex = -1;
|
|
|
|
// Add all the map-placed observer points
|
|
CBaseEntity *pObserverPoint = gEntList.FindEntityByClassname( NULL, "info_observer_point" );
|
|
while ( pObserverPoint )
|
|
{
|
|
m_hObservableEntities.AddToTail( pObserverPoint );
|
|
|
|
if ( m_hObserverTarget.Get() == pObserverPoint )
|
|
{
|
|
iCurrentIndex = (m_hObservableEntities.Count()-1);
|
|
}
|
|
|
|
pObserverPoint = gEntList.FindEntityByClassname( pObserverPoint, "info_observer_point" );
|
|
}
|
|
|
|
// Add all the players
|
|
for ( int i = 1; i <= gpGlobals->maxClients; i++ )
|
|
{
|
|
CBaseEntity *pPlayer = UTIL_PlayerByIndex( i );
|
|
if ( pPlayer )
|
|
{
|
|
m_hObservableEntities.AddToTail( pPlayer );
|
|
|
|
if ( m_hObserverTarget.Get() == pPlayer )
|
|
{
|
|
iCurrentIndex = (m_hObservableEntities.Count()-1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add all my objects
|
|
int iNumObjects = GetObjectCount();
|
|
for ( int i = 0; i < iNumObjects; i++ )
|
|
{
|
|
CBaseObject *pObj = GetObject(i);
|
|
if ( pObj )
|
|
{
|
|
m_hObservableEntities.AddToTail( pObj );
|
|
|
|
if ( m_hObserverTarget.Get() == pObj )
|
|
{
|
|
iCurrentIndex = (m_hObservableEntities.Count()-1);
|
|
}
|
|
}
|
|
}
|
|
|
|
return iCurrentIndex;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPlayer::GetNextObserverSearchStartPoint( bool bReverse )
|
|
{
|
|
int iDir = bReverse ? -1 : 1;
|
|
int startIndex = BuildObservableEntityList();
|
|
int iMax = m_hObservableEntities.Count()-1;
|
|
|
|
startIndex += iDir;
|
|
if (startIndex > iMax)
|
|
startIndex = 0;
|
|
else if (startIndex < 0)
|
|
startIndex = iMax;
|
|
|
|
return startIndex;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
CBaseEntity *CTFPlayer::FindNextObserverTarget(bool bReverse)
|
|
{
|
|
int startIndex = GetNextObserverSearchStartPoint( bReverse );
|
|
|
|
int currentIndex = startIndex;
|
|
int iDir = bReverse ? -1 : 1;
|
|
|
|
int iMax = m_hObservableEntities.Count()-1;
|
|
|
|
// Make sure the current index is within the max. Can happen if we were previously
|
|
// spectating an object which has been destroyed.
|
|
if ( startIndex > iMax )
|
|
{
|
|
currentIndex = startIndex = 1;
|
|
}
|
|
|
|
do
|
|
{
|
|
CBaseEntity *nextTarget = m_hObservableEntities[currentIndex];
|
|
|
|
if ( IsValidObserverTarget( nextTarget ) )
|
|
return nextTarget;
|
|
|
|
currentIndex += iDir;
|
|
|
|
// Loop through the entities
|
|
if (currentIndex > iMax)
|
|
{
|
|
currentIndex = 0;
|
|
}
|
|
else if (currentIndex < 0)
|
|
{
|
|
currentIndex = iMax;
|
|
}
|
|
} while ( currentIndex != startIndex );
|
|
|
|
return NULL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::IsValidObserverTarget(CBaseEntity * target)
|
|
{
|
|
if ( target && !target->IsPlayer() )
|
|
{
|
|
CObserverPoint *pObsPoint = dynamic_cast<CObserverPoint *>(target);
|
|
if ( pObsPoint && !pObsPoint->CanUseObserverPoint( this ) )
|
|
return false;
|
|
|
|
if ( GetTeamNumber() == TEAM_SPECTATOR )
|
|
return true;
|
|
|
|
switch ( mp_forcecamera.GetInt() )
|
|
{
|
|
case OBS_ALLOW_ALL : break;
|
|
case OBS_ALLOW_TEAM : if (target->GetTeamNumber() != TEAM_UNASSIGNED && GetTeamNumber() != target->GetTeamNumber())
|
|
return false;
|
|
break;
|
|
case OBS_ALLOW_NONE : return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return BaseClass::IsValidObserverTarget( target );
|
|
}
|
|
|
|
|
|
void CTFPlayer::PickWelcomeObserverPoint( void )
|
|
{
|
|
//Don't just spawn at the world origin, find a nice spot to look from while we choose our team and class.
|
|
CObserverPoint *pObserverPoint = (CObserverPoint *)gEntList.FindEntityByClassname( NULL, "info_observer_point" );
|
|
|
|
while ( pObserverPoint )
|
|
{
|
|
if ( IsValidObserverTarget( pObserverPoint ) )
|
|
{
|
|
SetObserverTarget( pObserverPoint );
|
|
}
|
|
|
|
if ( pObserverPoint->IsDefaultWelcome() )
|
|
break;
|
|
|
|
pObserverPoint = (CObserverPoint *)gEntList.FindEntityByClassname( pObserverPoint, "info_observer_point" );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::SetObserverTarget(CBaseEntity *target)
|
|
{
|
|
ClearZoomOwner();
|
|
SetFOV( this, 0 );
|
|
|
|
if ( !BaseClass::SetObserverTarget(target) )
|
|
return false;
|
|
|
|
CObserverPoint *pObsPoint = dynamic_cast<CObserverPoint *>(target);
|
|
if ( pObsPoint )
|
|
{
|
|
SetViewOffset( vec3_origin );
|
|
JumptoPosition( target->GetAbsOrigin(), target->EyeAngles() );
|
|
SetFOV( pObsPoint, pObsPoint->m_flFOV );
|
|
}
|
|
|
|
m_flLastAction = gpGlobals->curtime;
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Find the nearest team member within the distance of the origin.
|
|
// Favor players who are the same class.
|
|
//-----------------------------------------------------------------------------
|
|
CBaseEntity *CTFPlayer::FindNearestObservableTarget( Vector vecOrigin, float flMaxDist )
|
|
{
|
|
CTeam *pTeam = GetTeam();
|
|
CBaseEntity *pReturnTarget = NULL;
|
|
bool bFoundClass = false;
|
|
float flCurDistSqr = (flMaxDist * flMaxDist);
|
|
int iNumPlayers = pTeam->GetNumPlayers();
|
|
|
|
if ( pTeam->GetTeamNumber() == TEAM_SPECTATOR )
|
|
{
|
|
iNumPlayers = gpGlobals->maxClients;
|
|
}
|
|
|
|
|
|
for ( int i = 0; i < iNumPlayers; i++ )
|
|
{
|
|
CTFPlayer *pPlayer = NULL;
|
|
|
|
if ( pTeam->GetTeamNumber() == TEAM_SPECTATOR )
|
|
{
|
|
pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
|
|
}
|
|
else
|
|
{
|
|
pPlayer = ToTFPlayer( pTeam->GetPlayer(i) );
|
|
}
|
|
|
|
if ( !pPlayer )
|
|
continue;
|
|
|
|
if ( !IsValidObserverTarget(pPlayer) )
|
|
continue;
|
|
|
|
float flDistSqr = ( pPlayer->GetAbsOrigin() - vecOrigin ).LengthSqr();
|
|
|
|
if ( flDistSqr < flCurDistSqr )
|
|
{
|
|
// If we've found a player matching our class already, this guy needs
|
|
// to be a matching class and closer to boot.
|
|
if ( !bFoundClass || pPlayer->IsPlayerClass( GetPlayerClass()->GetClassIndex() ) )
|
|
{
|
|
pReturnTarget = pPlayer;
|
|
flCurDistSqr = flDistSqr;
|
|
|
|
if ( pPlayer->IsPlayerClass( GetPlayerClass()->GetClassIndex() ) )
|
|
{
|
|
bFoundClass = true;
|
|
}
|
|
}
|
|
}
|
|
else if ( !bFoundClass )
|
|
{
|
|
if ( pPlayer->IsPlayerClass( GetPlayerClass()->GetClassIndex() ) )
|
|
{
|
|
pReturnTarget = pPlayer;
|
|
flCurDistSqr = flDistSqr;
|
|
bFoundClass = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !bFoundClass && IsPlayerClass( TF_CLASS_ENGINEER ) )
|
|
{
|
|
// let's spectate our sentry instead, we didn't find any other engineers to spec
|
|
int iNumObjects = GetObjectCount();
|
|
for ( int i = 0; i < iNumObjects; i++ )
|
|
{
|
|
CBaseObject *pObj = GetObject(i);
|
|
|
|
if ( pObj && pObj->GetType() == OBJ_SENTRYGUN )
|
|
{
|
|
pReturnTarget = pObj;
|
|
}
|
|
}
|
|
}
|
|
|
|
return pReturnTarget;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::FindInitialObserverTarget( void )
|
|
{
|
|
// If we're on a team (i.e. not a pure observer), try and find
|
|
// a target that'll give the player the most useful information.
|
|
if ( GetTeamNumber() >= FIRST_GAME_TEAM )
|
|
{
|
|
CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
|
|
if ( pMaster )
|
|
{
|
|
// Has our forward cap point been contested recently?
|
|
int iFarthestPoint = TFGameRules()->GetFarthestOwnedControlPoint( GetTeamNumber(), false );
|
|
if ( iFarthestPoint != -1 )
|
|
{
|
|
float flTime = pMaster->PointLastContestedAt( iFarthestPoint );
|
|
if ( flTime != -1 && flTime > (gpGlobals->curtime - 30) )
|
|
{
|
|
// Does it have an associated viewpoint?
|
|
CBaseEntity *pObserverPoint = gEntList.FindEntityByClassname( NULL, "info_observer_point" );
|
|
while ( pObserverPoint )
|
|
{
|
|
CObserverPoint *pObsPoint = assert_cast<CObserverPoint *>(pObserverPoint);
|
|
if ( pObsPoint && pObsPoint->m_hAssociatedTeamEntity == pMaster->GetControlPoint(iFarthestPoint) )
|
|
{
|
|
if ( IsValidObserverTarget( pObsPoint ) )
|
|
{
|
|
m_hObserverTarget.Set( pObsPoint );
|
|
return;
|
|
}
|
|
}
|
|
|
|
pObserverPoint = gEntList.FindEntityByClassname( pObserverPoint, "info_observer_point" );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Has the point beyond our farthest been contested lately?
|
|
iFarthestPoint += (ObjectiveResource()->GetBaseControlPointForTeam( GetTeamNumber() ) == 0 ? 1 : -1);
|
|
if ( iFarthestPoint >= 0 && iFarthestPoint < MAX_CONTROL_POINTS )
|
|
{
|
|
float flTime = pMaster->PointLastContestedAt( iFarthestPoint );
|
|
if ( flTime != -1 && flTime > (gpGlobals->curtime - 30) )
|
|
{
|
|
// Try and find a player near that cap point
|
|
CBaseEntity *pCapPoint = pMaster->GetControlPoint(iFarthestPoint);
|
|
if ( pCapPoint )
|
|
{
|
|
CBaseEntity *pTarget = FindNearestObservableTarget( pCapPoint->GetAbsOrigin(), 1500 );
|
|
if ( pTarget )
|
|
{
|
|
m_hObserverTarget.Set( pTarget );
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find the nearest guy near myself
|
|
CBaseEntity *pTarget = FindNearestObservableTarget( GetAbsOrigin(), FLT_MAX );
|
|
if ( pTarget )
|
|
{
|
|
m_hObserverTarget.Set( pTarget );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::ValidateCurrentObserverTarget( void )
|
|
{
|
|
// If our current target is a dead player who's gibbed / died, refind as if
|
|
// we were finding our initial target, so we end up somewhere useful.
|
|
if ( m_hObserverTarget && m_hObserverTarget->IsPlayer() )
|
|
{
|
|
CBasePlayer *player = ToBasePlayer( m_hObserverTarget );
|
|
|
|
if ( player->m_lifeState == LIFE_DEAD || player->m_lifeState == LIFE_DYING )
|
|
{
|
|
// Once we're past the pause after death, find a new target
|
|
if ( (player->GetDeathTime() + DEATH_ANIMATION_TIME ) < gpGlobals->curtime )
|
|
{
|
|
FindInitialObserverTarget();
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( m_hObserverTarget && m_hObserverTarget->IsBaseObject() )
|
|
{
|
|
if ( m_iObserverMode == OBS_MODE_IN_EYE )
|
|
{
|
|
ForceObserverMode( OBS_MODE_CHASE );
|
|
}
|
|
}
|
|
|
|
BaseClass::ValidateCurrentObserverTarget();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::Touch( CBaseEntity *pOther )
|
|
{
|
|
CTFPlayer *pPlayer = ToTFPlayer( pOther );
|
|
|
|
if ( pPlayer )
|
|
{
|
|
CheckUncoveringSpies( pPlayer );
|
|
}
|
|
|
|
BaseClass::Touch( pOther );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Check to see if this player has seen through an enemy spy's disguise
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::CheckUncoveringSpies( CTFPlayer *pTouchedPlayer )
|
|
{
|
|
// Only uncover enemies
|
|
if ( m_Shared.IsAlly( pTouchedPlayer ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Only uncover if they're stealthed
|
|
if ( !pTouchedPlayer->m_Shared.InCond( TF_COND_STEALTHED ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// pulse their invisibility
|
|
pTouchedPlayer->m_Shared.OnSpyTouchedByEnemy();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::Taunt( void )
|
|
{
|
|
// Check to see if we can taunt again!
|
|
if ( m_Shared.InCond( TF_COND_TAUNTING ) )
|
|
return;
|
|
|
|
// Check to see if we are in water (above our waist).
|
|
if ( GetWaterLevel() > WL_Waist )
|
|
return;
|
|
|
|
// Check to see if we are on the ground.
|
|
if ( GetGroundEntity() == NULL )
|
|
return;
|
|
|
|
// Allow voice commands, etc to be interrupted.
|
|
CMultiplayer_Expresser *pExpresser = GetMultiplayerExpresser();
|
|
Assert( pExpresser );
|
|
pExpresser->AllowMultipleScenes();
|
|
|
|
m_bInitTaunt = true;
|
|
char szResponse[AI_Response::MAX_RESPONSE_NAME];
|
|
if ( SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_TAUNT, NULL, szResponse, AI_Response::MAX_RESPONSE_NAME ) )
|
|
{
|
|
// Get the duration of the scene.
|
|
float flDuration = GetSceneDuration( szResponse ) + 0.2f;
|
|
|
|
// Set player state as taunting.
|
|
m_Shared.AddCond( TF_COND_TAUNTING );
|
|
m_Shared.m_flTauntRemoveTime = gpGlobals->curtime + flDuration;
|
|
|
|
m_angTauntCamera = EyeAngles();
|
|
|
|
// Slam velocity to zero.
|
|
SetAbsVelocity( vec3_origin );
|
|
}
|
|
|
|
pExpresser->DisallowMultipleScenes();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Play a one-shot scene
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
float CTFPlayer::PlayScene( const char *pszScene, float flDelay, AI_Response *response, IRecipientFilter *filter )
|
|
{
|
|
// This is a lame way to detect a taunt!
|
|
if ( m_bInitTaunt )
|
|
{
|
|
m_bInitTaunt = false;
|
|
return InstancedScriptedScene( this, pszScene, &m_hTauntScene, flDelay, false, response, true, filter );
|
|
}
|
|
else
|
|
{
|
|
return InstancedScriptedScene( this, pszScene, NULL, flDelay, false, response, true, filter );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::ModifyOrAppendCriteria( AI_CriteriaSet& criteriaSet )
|
|
{
|
|
BaseClass::ModifyOrAppendCriteria( criteriaSet );
|
|
|
|
// If we have 'disguiseclass' criteria, pretend that we are actually our
|
|
// disguise class. That way we just look up the scene we would play as if
|
|
// we were that class.
|
|
int disguiseIndex = criteriaSet.FindCriterionIndex( "disguiseclass" );
|
|
|
|
if ( disguiseIndex != -1 )
|
|
{
|
|
criteriaSet.AppendCriteria( "playerclass", criteriaSet.GetValue(disguiseIndex) );
|
|
}
|
|
else
|
|
{
|
|
if ( GetPlayerClass() )
|
|
{
|
|
criteriaSet.AppendCriteria( "playerclass", g_aPlayerClassNames_NonLocalized[ GetPlayerClass()->GetClassIndex() ] );
|
|
}
|
|
}
|
|
|
|
criteriaSet.AppendCriteria( "recentkills", UTIL_VarArgs("%d", m_Shared.GetNumKillsInTime(30.0)) );
|
|
|
|
int iTotalKills = 0;
|
|
PlayerStats_t *pStats = CTF_GameStats.FindPlayerStats( this );
|
|
if ( pStats )
|
|
{
|
|
iTotalKills = pStats->statsCurrentLife.m_iStat[TFSTAT_KILLS] + pStats->statsCurrentLife.m_iStat[TFSTAT_KILLASSISTS]+
|
|
pStats->statsCurrentLife.m_iStat[TFSTAT_BUILDINGSDESTROYED];
|
|
}
|
|
criteriaSet.AppendCriteria( "killsthislife", UTIL_VarArgs( "%d", iTotalKills ) );
|
|
criteriaSet.AppendCriteria( "disguised", m_Shared.InCond( TF_COND_DISGUISED ) ? "1" : "0" );
|
|
criteriaSet.AppendCriteria( "invulnerable", m_Shared.InCond( TF_COND_INVULNERABLE ) ? "1" : "0" );
|
|
criteriaSet.AppendCriteria( "beinghealed", m_Shared.InCond( TF_COND_HEALTH_BUFF ) ? "1" : "0" );
|
|
criteriaSet.AppendCriteria( "waitingforplayers", (TFGameRules()->IsInWaitingForPlayers() || TFGameRules()->IsInPreMatch()) ? "1" : "0" );
|
|
|
|
// Current weapon role
|
|
CTFWeaponBase *pActiveWeapon = m_Shared.GetActiveTFWeapon();
|
|
if ( pActiveWeapon )
|
|
{
|
|
int iWeaponRole = pActiveWeapon->GetTFWpnData().m_iWeaponType;
|
|
switch( iWeaponRole )
|
|
{
|
|
case TF_WPN_TYPE_PRIMARY:
|
|
default:
|
|
criteriaSet.AppendCriteria( "weaponmode", "primary" );
|
|
break;
|
|
case TF_WPN_TYPE_SECONDARY:
|
|
criteriaSet.AppendCriteria( "weaponmode", "secondary" );
|
|
break;
|
|
case TF_WPN_TYPE_MELEE:
|
|
criteriaSet.AppendCriteria( "weaponmode", "melee" );
|
|
break;
|
|
case TF_WPN_TYPE_BUILDING:
|
|
criteriaSet.AppendCriteria( "weaponmode", "building" );
|
|
break;
|
|
case TF_WPN_TYPE_PDA:
|
|
criteriaSet.AppendCriteria( "weaponmode", "pda" );
|
|
break;
|
|
}
|
|
|
|
if ( pActiveWeapon->GetWeaponID() == TF_WEAPON_SNIPERRIFLE )
|
|
{
|
|
CTFSniperRifle *pRifle = dynamic_cast<CTFSniperRifle*>(pActiveWeapon);
|
|
if ( pRifle && pRifle->IsZoomed() )
|
|
{
|
|
criteriaSet.AppendCriteria( "sniperzoomed", "1" );
|
|
}
|
|
}
|
|
else if ( pActiveWeapon->GetWeaponID() == TF_WEAPON_MINIGUN )
|
|
{
|
|
CTFMinigun *pMinigun = dynamic_cast<CTFMinigun*>(pActiveWeapon);
|
|
if ( pMinigun )
|
|
{
|
|
criteriaSet.AppendCriteria( "minigunfiretime", UTIL_VarArgs("%.1f", pMinigun->GetFiringTime() ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Player under crosshair
|
|
trace_t tr;
|
|
Vector forward;
|
|
EyeVectors( &forward );
|
|
UTIL_TraceLine( EyePosition(), EyePosition() + (forward * MAX_TRACE_LENGTH), MASK_BLOCKLOS_AND_NPCS, this, COLLISION_GROUP_NONE, &tr );
|
|
if ( !tr.startsolid && tr.DidHitNonWorldEntity() )
|
|
{
|
|
CBaseEntity *pEntity = tr.m_pEnt;
|
|
if ( pEntity && pEntity->IsPlayer() )
|
|
{
|
|
CTFPlayer *pTFPlayer = ToTFPlayer(pEntity);
|
|
if ( pTFPlayer )
|
|
{
|
|
int iClass = pTFPlayer->GetPlayerClass()->GetClassIndex();
|
|
if ( !InSameTeam(pTFPlayer) )
|
|
{
|
|
// Prevent spotting stealthed enemies who haven't been exposed recently
|
|
if ( pTFPlayer->m_Shared.InCond( TF_COND_STEALTHED ) )
|
|
{
|
|
if ( pTFPlayer->m_Shared.GetLastStealthExposedTime() < (gpGlobals->curtime - 3.0) )
|
|
{
|
|
iClass = TF_CLASS_UNDEFINED;
|
|
}
|
|
else
|
|
{
|
|
iClass = TF_CLASS_SPY;
|
|
}
|
|
}
|
|
else if ( pTFPlayer->m_Shared.InCond( TF_COND_DISGUISED ) )
|
|
{
|
|
iClass = pTFPlayer->m_Shared.GetDisguiseClass();
|
|
}
|
|
}
|
|
|
|
if ( iClass > TF_CLASS_UNDEFINED && iClass <= TF_LAST_NORMAL_CLASS )
|
|
{
|
|
criteriaSet.AppendCriteria( "crosshair_on", g_aPlayerClassNames_NonLocalized[iClass] );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Previous round win
|
|
bool bLoser = ( TFGameRules()->GetPreviousRoundWinners() != TEAM_UNASSIGNED && TFGameRules()->GetPreviousRoundWinners() != GetTeamNumber() );
|
|
criteriaSet.AppendCriteria( "LostRound", UTIL_VarArgs("%d", bLoser) );
|
|
|
|
// Control points
|
|
touchlink_t *root = ( touchlink_t * )GetDataObject( TOUCHLINK );
|
|
if ( root )
|
|
{
|
|
for ( touchlink_t *link = root->nextLink; link != root; link = link->nextLink )
|
|
{
|
|
CBaseEntity *pTouch = link->entityTouched;
|
|
if ( pTouch && pTouch->IsSolidFlagSet( FSOLID_TRIGGER ) && pTouch->IsBSPModel() )
|
|
{
|
|
CTriggerAreaCapture *pAreaTrigger = dynamic_cast<CTriggerAreaCapture*>(pTouch);
|
|
if ( pAreaTrigger )
|
|
{
|
|
CTeamControlPoint *pCP = pAreaTrigger->GetControlPoint();
|
|
if ( pCP )
|
|
{
|
|
if ( pCP->GetOwner() == GetTeamNumber() )
|
|
{
|
|
criteriaSet.AppendCriteria( "OnFriendlyControlPoint", "1" );
|
|
}
|
|
else
|
|
{
|
|
if ( TeamplayGameRules()->TeamMayCapturePoint( GetTeamNumber(), pCP->GetPointIndex() ) &&
|
|
TeamplayGameRules()->PlayerMayCapturePoint( this, pCP->GetPointIndex() ) )
|
|
{
|
|
criteriaSet.AppendCriteria( "OnCappableControlPoint", "1" );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::CanHearAndReadChatFrom( CBasePlayer *pPlayer )
|
|
{
|
|
// can always hear the console unless we're ignoring all chat
|
|
if ( !pPlayer )
|
|
return m_iIgnoreGlobalChat != CHAT_IGNORE_ALL;
|
|
|
|
// check if we're ignoring all chat
|
|
if ( m_iIgnoreGlobalChat == CHAT_IGNORE_ALL )
|
|
return false;
|
|
|
|
// check if we're ignoring all but teammates
|
|
if ( m_iIgnoreGlobalChat == CHAT_IGNORE_TEAM && g_pGameRules->PlayerRelationship( this, pPlayer ) != GR_TEAMMATE )
|
|
return false;
|
|
|
|
if ( !pPlayer->IsAlive() && IsAlive() )
|
|
{
|
|
// Everyone can chat like normal when the round/game ends
|
|
if ( TFGameRules()->State_Get() == GR_STATE_TEAM_WIN || TFGameRules()->State_Get() == GR_STATE_GAME_OVER )
|
|
return true;
|
|
|
|
// Separate rule for spectators.
|
|
if ( pPlayer->GetTeamNumber() < FIRST_GAME_TEAM )
|
|
return tf_spectalk.GetBool();
|
|
|
|
// Living players can't hear dead ones unless gravetalk is enabled.
|
|
return tf_gravetalk.GetBool();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
IResponseSystem *CTFPlayer::GetResponseSystem()
|
|
{
|
|
int iClass = GetPlayerClass()->GetClassIndex();
|
|
|
|
if ( m_bSpeakingConceptAsDisguisedSpy && m_Shared.InCond( TF_COND_DISGUISED ) )
|
|
{
|
|
iClass = m_Shared.GetDisguiseClass();
|
|
}
|
|
|
|
bool bValidClass = ( iClass >= TF_CLASS_SCOUT && iClass <= TF_LAST_NORMAL_CLASS );
|
|
bool bValidConcept = ( m_iCurrentConcept >= 0 && m_iCurrentConcept < MP_TF_CONCEPT_COUNT );
|
|
Assert( bValidClass );
|
|
Assert( bValidConcept );
|
|
|
|
if ( !bValidClass || !bValidConcept )
|
|
{
|
|
return BaseClass::GetResponseSystem();
|
|
}
|
|
else
|
|
{
|
|
return TFGameRules()->m_ResponseRules[iClass].m_ResponseSystems[m_iCurrentConcept];
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::SpeakConceptIfAllowed( int iConcept, const char *modifiers, char *pszOutResponseChosen, size_t bufsize, IRecipientFilter *filter )
|
|
{
|
|
if ( !IsAlive() )
|
|
return false;
|
|
|
|
bool bReturn = false;
|
|
|
|
if ( IsSpeaking() )
|
|
{
|
|
if ( iConcept != MP_CONCEPT_DIED )
|
|
return false;
|
|
}
|
|
|
|
// Save the current concept.
|
|
m_iCurrentConcept = iConcept;
|
|
|
|
if ( m_Shared.InCond( TF_COND_DISGUISED ) && !filter )
|
|
{
|
|
CSingleUserRecipientFilter filter(this);
|
|
|
|
int iEnemyTeam = ( GetTeamNumber() == TF_TEAM_RED ) ? TF_TEAM_BLUE : TF_TEAM_RED;
|
|
|
|
// test, enemies and myself
|
|
CTeamRecipientFilter disguisedFilter( iEnemyTeam );
|
|
disguisedFilter.AddRecipient( this );
|
|
|
|
CMultiplayer_Expresser *pExpresser = GetMultiplayerExpresser();
|
|
Assert( pExpresser );
|
|
|
|
pExpresser->AllowMultipleScenes();
|
|
|
|
// play disguised concept to enemies and myself
|
|
char buf[128];
|
|
Q_snprintf( buf, sizeof(buf), "disguiseclass:%s", g_aPlayerClassNames_NonLocalized[ m_Shared.GetDisguiseClass() ] );
|
|
|
|
if ( modifiers )
|
|
{
|
|
Q_strncat( buf, ",", sizeof(buf), 1 );
|
|
Q_strncat( buf, modifiers, sizeof(buf), COPY_ALL_CHARACTERS );
|
|
}
|
|
|
|
m_bSpeakingConceptAsDisguisedSpy = true;
|
|
|
|
bool bPlayedDisguised = SpeakIfAllowed( g_pszMPConcepts[iConcept], buf, pszOutResponseChosen, bufsize, &disguisedFilter );
|
|
|
|
m_bSpeakingConceptAsDisguisedSpy = false;
|
|
|
|
// test, everyone except enemies and myself
|
|
CBroadcastRecipientFilter undisguisedFilter;
|
|
undisguisedFilter.RemoveRecipientsByTeam( GetGlobalTFTeam(iEnemyTeam) );
|
|
undisguisedFilter.RemoveRecipient( this );
|
|
|
|
// play normal concept to teammates
|
|
bool bPlayedNormally = SpeakIfAllowed( g_pszMPConcepts[iConcept], modifiers, pszOutResponseChosen, bufsize, &undisguisedFilter );
|
|
|
|
pExpresser->DisallowMultipleScenes();
|
|
|
|
bReturn = ( bPlayedDisguised && bPlayedNormally );
|
|
}
|
|
else
|
|
{
|
|
// play normally
|
|
bReturn = SpeakIfAllowed( g_pszMPConcepts[iConcept], modifiers, pszOutResponseChosen, bufsize, filter );
|
|
}
|
|
|
|
//Add bubble on top of a player calling for medic.
|
|
if ( bReturn )
|
|
{
|
|
if ( iConcept == MP_CONCEPT_PLAYER_MEDIC )
|
|
{
|
|
SaveMe();
|
|
}
|
|
}
|
|
|
|
return bReturn;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::UpdateExpression( void )
|
|
{
|
|
char szScene[ MAX_PATH ];
|
|
if ( !GetResponseSceneFromConcept( MP_CONCEPT_PLAYER_EXPRESSION, szScene, sizeof( szScene ) ) )
|
|
{
|
|
ClearExpression();
|
|
m_flNextRandomExpressionTime = gpGlobals->curtime + RandomFloat(30,40);
|
|
return;
|
|
}
|
|
|
|
// Ignore updates that choose the same scene
|
|
if ( m_iszExpressionScene != NULL_STRING && stricmp( STRING(m_iszExpressionScene), szScene ) == 0 )
|
|
return;
|
|
|
|
if ( m_hExpressionSceneEnt )
|
|
{
|
|
ClearExpression();
|
|
}
|
|
|
|
m_iszExpressionScene = AllocPooledString( szScene );
|
|
float flDuration = InstancedScriptedScene( this, szScene, &m_hExpressionSceneEnt, 0.0, true, NULL, true );
|
|
m_flNextRandomExpressionTime = gpGlobals->curtime + flDuration;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::ClearExpression( void )
|
|
{
|
|
if ( m_hExpressionSceneEnt != NULL )
|
|
{
|
|
StopScriptedScene( this, m_hExpressionSceneEnt );
|
|
}
|
|
m_flNextRandomExpressionTime = gpGlobals->curtime;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Only show subtitle to enemy if we're disguised as the enemy
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::ShouldShowVoiceSubtitleToEnemy( void )
|
|
{
|
|
return ( m_Shared.InCond( TF_COND_DISGUISED ) && m_Shared.GetDisguiseTeam() != GetTeamNumber() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Don't allow rapid-fire voice commands
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::CanSpeakVoiceCommand( void )
|
|
{
|
|
return ( gpGlobals->curtime > m_flNextVoiceCommandTime );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Note the time we're allowed to next speak a voice command
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::NoteSpokeVoiceCommand( const char *pszScenePlayed )
|
|
{
|
|
Assert( pszScenePlayed );
|
|
m_flNextVoiceCommandTime = gpGlobals->curtime + MIN( GetSceneDuration( pszScenePlayed ), tf_max_voice_speak_delay.GetFloat() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::WantsLagCompensationOnEntity( const CBasePlayer *pPlayer, const CUserCmd *pCmd, const CBitVec<MAX_EDICTS> *pEntityTransmitBits ) const
|
|
{
|
|
bool bIsMedic = false;
|
|
|
|
//Do Lag comp on medics trying to heal team mates.
|
|
if ( IsPlayerClass( TF_CLASS_MEDIC ) == true )
|
|
{
|
|
bIsMedic = true;
|
|
|
|
if ( pPlayer->GetTeamNumber() == GetTeamNumber() )
|
|
{
|
|
CWeaponMedigun *pWeapon = dynamic_cast <CWeaponMedigun*>( GetActiveWeapon() );
|
|
|
|
if ( pWeapon && pWeapon->GetHealTarget() )
|
|
{
|
|
if ( pWeapon->GetHealTarget() == pPlayer )
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( pPlayer->GetTeamNumber() == GetTeamNumber() && bIsMedic == false )
|
|
return false;
|
|
|
|
// If this entity hasn't been transmitted to us and acked, then don't bother lag compensating it.
|
|
if ( pEntityTransmitBits && !pEntityTransmitBits->Get( pPlayer->entindex() ) )
|
|
return false;
|
|
|
|
const Vector &vMyOrigin = GetAbsOrigin();
|
|
const Vector &vHisOrigin = pPlayer->GetAbsOrigin();
|
|
|
|
// get max distance player could have moved within max lag compensation time,
|
|
// multiply by 1.5 to to avoid "dead zones" (sqrt(2) would be the exact value)
|
|
float maxDistance = 1.5 * pPlayer->MaxSpeed() * sv_maxunlag.GetFloat();
|
|
|
|
// If the player is within this distance, lag compensate them in case they're running past us.
|
|
if ( vHisOrigin.DistTo( vMyOrigin ) < maxDistance )
|
|
return true;
|
|
|
|
// If their origin is not within a 45 degree cone in front of us, no need to lag compensate.
|
|
Vector vForward;
|
|
AngleVectors( pCmd->viewangles, &vForward );
|
|
|
|
Vector vDiff = vHisOrigin - vMyOrigin;
|
|
VectorNormalize( vDiff );
|
|
|
|
float flCosAngle = 0.707107f; // 45 degree angle
|
|
if ( vForward.Dot( vDiff ) < flCosAngle )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::SpeakWeaponFire( int iCustomConcept )
|
|
{
|
|
if ( iCustomConcept == MP_CONCEPT_NONE )
|
|
{
|
|
if ( m_flNextSpeakWeaponFire > gpGlobals->curtime )
|
|
return;
|
|
|
|
iCustomConcept = MP_CONCEPT_FIREWEAPON;
|
|
}
|
|
|
|
m_flNextSpeakWeaponFire = gpGlobals->curtime + 5;
|
|
|
|
// Don't play a weapon fire scene if we already have one
|
|
if ( m_hWeaponFireSceneEnt )
|
|
return;
|
|
|
|
char szScene[ MAX_PATH ];
|
|
if ( !GetResponseSceneFromConcept( iCustomConcept, szScene, sizeof( szScene ) ) )
|
|
return;
|
|
|
|
float flDuration = InstancedScriptedScene( this, szScene, &m_hExpressionSceneEnt, 0.0, true, NULL, true );
|
|
m_flNextSpeakWeaponFire = gpGlobals->curtime + flDuration;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::ClearWeaponFireScene( void )
|
|
{
|
|
if ( m_hWeaponFireSceneEnt )
|
|
{
|
|
StopScriptedScene( this, m_hWeaponFireSceneEnt );
|
|
m_hWeaponFireSceneEnt = NULL;
|
|
}
|
|
m_flNextSpeakWeaponFire = gpGlobals->curtime;
|
|
}
|
|
|
|
int CTFPlayer::DrawDebugTextOverlays(void)
|
|
{
|
|
int text_offset = BaseClass::DrawDebugTextOverlays();
|
|
|
|
if (m_debugOverlays & OVERLAY_TEXT_BIT)
|
|
{
|
|
char tempstr[512];
|
|
|
|
Q_snprintf( tempstr, sizeof( tempstr ),"Health: %d / %d ( %.1f )", GetHealth(), GetMaxHealth(), (float)GetHealth() / (float)GetMaxHealth() );
|
|
EntityText(text_offset,tempstr,0);
|
|
text_offset++;
|
|
}
|
|
return text_offset;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Get response scene corresponding to concept
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::GetResponseSceneFromConcept( int iConcept, char *chSceneBuffer, int numSceneBufferBytes )
|
|
{
|
|
AI_Response response;
|
|
bool bResult = false; //SpeakConcept( response, iConcept );
|
|
|
|
if ( bResult )
|
|
{
|
|
if ( response.IsApplyContextToWorld() )
|
|
{
|
|
CBaseEntity *pEntity = CBaseEntity::Instance( engine->PEntityOfEntIndex( 0 ) );
|
|
if ( pEntity )
|
|
{
|
|
pEntity->AddContext( response.GetContext() );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AddContext( response.GetContext() );
|
|
}
|
|
|
|
V_strncpy( chSceneBuffer, response.GetResponsePtr(), numSceneBufferBytes );
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:calculate a score for this player. higher is more likely to be switched
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPlayer::CalculateTeamBalanceScore( void )
|
|
{
|
|
int iScore = BaseClass::CalculateTeamBalanceScore();
|
|
|
|
// switch engineers less often
|
|
if ( IsPlayerClass( TF_CLASS_ENGINEER ) )
|
|
{
|
|
iScore -= 120;
|
|
}
|
|
|
|
return iScore;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
// Debugging Stuff
|
|
extern CBaseEntity *FindPickerEntity( CBasePlayer *pPlayer );
|
|
void DebugParticles( const CCommand &args )
|
|
{
|
|
CBaseEntity *pEntity = FindPickerEntity( UTIL_GetCommandClient() );
|
|
|
|
if ( pEntity && pEntity->IsPlayer() )
|
|
{
|
|
CTFPlayer *pPlayer = ToTFPlayer( pEntity );
|
|
|
|
// print out their conditions
|
|
pPlayer->m_Shared.DebugPrintConditions();
|
|
}
|
|
}
|
|
|
|
static ConCommand sv_debug_stuck_particles( "sv_debug_stuck_particles", DebugParticles, "Debugs particles attached to the player under your crosshair.", FCVAR_DEVELOPMENTONLY );
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Debug concommand to set the player on fire
|
|
//-----------------------------------------------------------------------------
|
|
void IgnitePlayer()
|
|
{
|
|
CTFPlayer *pPlayer = ToTFPlayer( ToTFPlayer( UTIL_PlayerByIndex( 1 ) ) );
|
|
pPlayer->m_Shared.Burn( pPlayer );
|
|
}
|
|
static ConCommand cc_IgnitePlayer( "tf_ignite_player", IgnitePlayer, "Sets you on fire", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void TestVCD( const CCommand &args )
|
|
{
|
|
CBaseEntity *pEntity = FindPickerEntity( UTIL_GetCommandClient() );
|
|
if ( pEntity && pEntity->IsPlayer() )
|
|
{
|
|
CTFPlayer *pPlayer = ToTFPlayer( pEntity );
|
|
if ( pPlayer )
|
|
{
|
|
if ( args.ArgC() >= 2 )
|
|
{
|
|
InstancedScriptedScene( pPlayer, args[1], NULL, 0.0f, false, NULL, true );
|
|
}
|
|
else
|
|
{
|
|
InstancedScriptedScene( pPlayer, "scenes/heavy_test.vcd", NULL, 0.0f, false, NULL, true );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static ConCommand tf_testvcd( "tf_testvcd", TestVCD, "Run a vcd on the player currently under your crosshair. Optional parameter is the .vcd name (default is 'scenes/heavy_test.vcd')", FCVAR_CHEAT );
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void TestRR( const CCommand &args )
|
|
{
|
|
if ( args.ArgC() < 2 )
|
|
{
|
|
Msg("No concept specified. Format is tf_testrr <concept>\n");
|
|
return;
|
|
}
|
|
|
|
CBaseEntity *pEntity = NULL;
|
|
const char *pszConcept = args[1];
|
|
|
|
if ( args.ArgC() == 3 )
|
|
{
|
|
pszConcept = args[2];
|
|
pEntity = UTIL_PlayerByName( args[1] );
|
|
}
|
|
|
|
if ( !pEntity || !pEntity->IsPlayer() )
|
|
{
|
|
pEntity = FindPickerEntity( UTIL_GetCommandClient() );
|
|
if ( !pEntity || !pEntity->IsPlayer() )
|
|
{
|
|
pEntity = ToTFPlayer( UTIL_GetCommandClient() );
|
|
}
|
|
}
|
|
|
|
if ( pEntity && pEntity->IsPlayer() )
|
|
{
|
|
CTFPlayer *pPlayer = ToTFPlayer( pEntity );
|
|
if ( pPlayer )
|
|
{
|
|
int iConcept = GetMPConceptIndexFromString( pszConcept );
|
|
if ( iConcept != MP_CONCEPT_NONE )
|
|
{
|
|
pPlayer->SpeakConceptIfAllowed( iConcept );
|
|
}
|
|
else
|
|
{
|
|
Msg( "Attempted to speak unknown multiplayer concept: %s\n", pszConcept );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static ConCommand tf_testrr( "tf_testrr", TestRR, "Force the player under your crosshair to speak a response rule concept. Format is tf_testrr <concept>, or tf_testrr <player name> <concept>", FCVAR_CHEAT );
|
|
|
|
|
|
CON_COMMAND_F( tf_crashclients, "testing only, crashes about 50 percent of the connected clients.", FCVAR_DEVELOPMENTONLY )
|
|
{
|
|
for ( int i = 1; i < gpGlobals->maxClients; ++i )
|
|
{
|
|
if ( RandomFloat( 0.0f, 1.0f ) < 0.5f )
|
|
{
|
|
CBasePlayer *pl = UTIL_PlayerByIndex( i + 1 );
|
|
if ( pl )
|
|
{
|
|
engine->ClientCommand( pl->edict(), "crash\n" );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::SetPowerplayEnabled( bool bOn )
|
|
{
|
|
if ( bOn )
|
|
{
|
|
m_flPowerPlayTime = gpGlobals->curtime + 99999;
|
|
m_Shared.RecalculateInvuln();
|
|
m_Shared.Burn( this );
|
|
|
|
PowerplayThink();
|
|
}
|
|
else
|
|
{
|
|
m_flPowerPlayTime = 0.0;
|
|
m_Shared.RemoveCond( TF_COND_BURNING );
|
|
m_Shared.RecalculateInvuln();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
uint64 powerplaymask = 0xFAB2423BFFA352AF;
|
|
uint64 powerplay_ids[] =
|
|
{
|
|
76561197960435530 ^ powerplaymask,
|
|
76561197960265731 ^ powerplaymask,
|
|
76561197960265749 ^ powerplaymask,
|
|
76561197962783665 ^ powerplaymask,
|
|
};
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::PlayerHasPowerplay( void )
|
|
{
|
|
if ( !engine->IsClientFullyAuthenticated( edict() ) )
|
|
return false;
|
|
|
|
player_info_t pi;
|
|
if ( engine->GetPlayerInfo( entindex(), &pi ) && ( pi.friendsID ) )
|
|
{
|
|
CSteamID steamIDForPlayer( pi.friendsID, 1, k_EUniversePublic, k_EAccountTypeIndividual );
|
|
for ( int i = 0; i < ARRAYSIZE(powerplay_ids); i++ )
|
|
{
|
|
if ( steamIDForPlayer.ConvertToUint64() == (powerplay_ids[i] ^ powerplaymask) )
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPlayer::PowerplayThink( void )
|
|
{
|
|
if ( m_flPowerPlayTime > gpGlobals->curtime )
|
|
{
|
|
float flDuration = 0;
|
|
if ( GetPlayerClass() )
|
|
{
|
|
switch ( GetPlayerClass()->GetClassIndex() )
|
|
{
|
|
case TF_CLASS_SCOUT: flDuration = InstancedScriptedScene( this, "scenes/player/scout/low/laughlong02.vcd", NULL, 0.0f, false, NULL, true ); break;
|
|
case TF_CLASS_SNIPER: flDuration = InstancedScriptedScene( this, "scenes/player/sniper/low/laughlong01.vcd", NULL, 0.0f, false, NULL, true ); break;
|
|
case TF_CLASS_SOLDIER: flDuration = InstancedScriptedScene( this, "scenes/player/soldier/low/laughevil02.vcd", NULL, 0.0f, false, NULL, true ); break;
|
|
case TF_CLASS_DEMOMAN: flDuration = InstancedScriptedScene( this, "scenes/player/demoman/low/laughlong02.vcd", NULL, 0.0f, false, NULL, true ); break;
|
|
case TF_CLASS_MEDIC: flDuration = InstancedScriptedScene( this, "scenes/player/medic/low/laughlong02.vcd", NULL, 0.0f, false, NULL, true ); break;
|
|
case TF_CLASS_HEAVYWEAPONS: flDuration = InstancedScriptedScene( this, "scenes/player/heavy/low/laughlong01.vcd", NULL, 0.0f, false, NULL, true ); break;
|
|
case TF_CLASS_PYRO: flDuration = InstancedScriptedScene( this, "scenes/player/pyro/low/laughlong01.vcd", NULL, 0.0f, false, NULL, true ); break;
|
|
case TF_CLASS_SPY: flDuration = InstancedScriptedScene( this, "scenes/player/spy/low/laughevil01.vcd", NULL, 0.0f, false, NULL, true ); break;
|
|
case TF_CLASS_ENGINEER: flDuration = InstancedScriptedScene( this, "scenes/player/engineer/low/laughlong01.vcd", NULL, 0.0f, false, NULL, true ); break;
|
|
}
|
|
}
|
|
|
|
SetContextThink( &CTFPlayer::PowerplayThink, gpGlobals->curtime + flDuration + RandomFloat( 2, 5 ), "TFPlayerLThink" );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPlayer::ShouldAnnouceAchievement( void )
|
|
{
|
|
if ( IsPlayerClass( TF_CLASS_SPY ) )
|
|
{
|
|
if ( m_Shared.InCond( TF_COND_STEALTHED ) ||
|
|
m_Shared.InCond( TF_COND_DISGUISED ) ||
|
|
m_Shared.InCond( TF_COND_DISGUISING ) )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|