//========= 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 . 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<= 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( 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( 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;iGetType() == 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( 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( 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( 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( 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( 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(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. 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 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( 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( m_hRagdoll.Get() ); if( pRagdoll ) { UTIL_Remove( pRagdoll ); pRagdoll = NULL; } Assert( pRagdoll == NULL ); // Create a ragdoll. pRagdoll = dynamic_cast( 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( 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(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(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(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(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(pActiveWeapon); if ( pRifle && pRifle->IsZoomed() ) { criteriaSet.AppendCriteria( "sniperzoomed", "1" ); } } else if ( pActiveWeapon->GetWeaponID() == TF_WEAPON_MINIGUN ) { CTFMinigun *pMinigun = dynamic_cast(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(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 *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 ( 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 = 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 \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 , or tf_testrr ", 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; }