diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/tf/tf_player.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/server/tf/tf_player.cpp')
| -rw-r--r-- | game/server/tf/tf_player.cpp | 22005 |
1 files changed, 22005 insertions, 0 deletions
diff --git a/game/server/tf/tf_player.cpp b/game/server/tf/tf_player.cpp new file mode 100644 index 0000000..00a8c1a --- /dev/null +++ b/game/server/tf/tf_player.cpp @@ -0,0 +1,22005 @@ +//========= Copyright Valve Corporation, 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 "tf_weapon_fists.h" +#include "tf_weapon_shotgun.h" +#include "tf_weapon_lunchbox.h" +#include "tf_weapon_knife.h" +#include "tf_weapon_bottle.h" +#include "tf_weapon_sword.h" +#include "tf_weapon_grenade_pipebomb.h" +#include "tf_weapon_buff_item.h" +#include "tf_weapon_flamethrower.h" +#include "tf_projectile_flare.h" +#include "trigger_area_capture.h" +#include "triggers.h" +#include "tf_weapon_medigun.h" +#include "tf_weapon_invis.h" +#include "hl2orange.spa.h" +#include "te_tfblood.h" +#include "activitylist.h" +#include "cdll_int.h" +#include "econ_entity_creation.h" +#include "tf_weaponbase_gun.h" +#include "team_train_watcher.h" +#include "vgui/ILocalize.h" +#include "tier3/tier3.h" +#include "serverbenchmark_base.h" +#include "trains.h" +#include "tf_fx.h" +#include "recipientfilter.h" +#include "ilagcompensationmanager.h" +#include "dt_utlvector_send.h" +#include "tf_item_wearable.h" +#include "tf_item_powerup_bottle.h" +#include "nav_mesh/tf_nav_mesh.h" +#include "tier0/vprof.h" +#include "econ_gcmessages.h" +#include "tf_gcmessages.h" +#include "tf_obj_sentrygun.h" +#include "tf_weapon_shovel.h" +#include "bot/tf_bot.h" +#include "bot/tf_bot_manager.h" +#include "NextBotUtil.h" +#include "tf_wearable_item_demoshield.h" +#include "tier0/icommandline.h" +#include "entity_healthkit.h" +#include "choreoevent.h" +#include "minigames/tf_duel.h" +#include "tf_bot_temp.h" +#include "tf_objective_resource.h" +#include "tf_weapon_pipebomblauncher.h" +#include "func_achievement.h" +#include "halloween/merasmus/merasmus.h" +#include "inetchannel.h" +#include "tf_wearable_levelable_item.h" +#include "tf_weapon_jar.h" +#include "halloween/tf_weapon_spellbook.h" +#include "soundenvelope.h" +#include "tf_triggers.h" +#include "collisionutils.h" +#include "tf_taunt_prop.h" +#include "eventlist.h" +#include "entity_rune.h" +#include "entity_halloween_pickup.h" +#include "tf_gc_server.h" +#include "tf_logic_halloween_2014.h" +#include "tf_weapon_knife.h" +#include "tf_weapon_grapplinghook.h" +#include "tf_dropped_weapon.h" +#include "tf_passtime_logic.h" +#include "tf_weapon_passtime_gun.h" +#include "player_resource.h" +#include "tf_player_resource.h" +#include "gcsdk/gcclient_sharedobjectcache.h" +#include "tf_party.h" +#ifdef STAGING_ONLY +#include "tf_extra_map_entity.h" +#endif + +#ifdef TF_RAID_MODE +#include "bot_npc/bot_npc_decoy.h" +#include "raid/tf_raid_logic.h" +#endif + +#include "entity_currencypack.h" +#include "tf_mann_vs_machine_stats.h" +#include "player_vs_environment/tf_upgrades.h" +#include "player_vs_environment/tf_population_manager.h" +#include "tf_revive.h" +#include "tf_logic_halloween_2014.h" +#include "tf_logic_player_destruction.h" + +// NVNT haptic utils +#include "haptics/haptic_utils.h" + +#include "gc_clientsystem.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#pragma warning( disable: 4355 ) // disables ' 'this' : used in base member initializer list' + +ConVar sv_motd_unload_on_dismissal( "sv_motd_unload_on_dismissal", "0", 0, "If enabled, the MOTD contents will be unloaded when the player closes the MOTD." ); + +#define DAMAGE_FORCE_SCALE_SELF 9 +#define SCOUT_ADD_BIRD_ON_GIB_CHANCE 5 +#define MEDIC_RELEASE_DOVE_COUNT 10 + +#define JUMP_MIN_SPEED 268.3281572999747f + +extern bool IsInCommentaryMode( void ); +extern void SpawnClientsideFlyingBird( Vector &vecSpawn ); + +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; +extern ConVar tf_gravetalk; + +extern ConVar tf_bot_quota_mode; +extern ConVar tf_bot_quota; +extern ConVar halloween_starting_souls; + +float GetCurrentGravity( void ); + +float m_flNextReflectZap = 0.f; + +static CTFPlayer *gs_pRecursivePlayerCheck = NULL; + +bool CTFPlayer::m_bTFPlayerNeedsPrecache = true; + +static const char g_pszIdleKickString[] = "#TF_Idle_kicked"; + +EHANDLE g_pLastSpawnPoints[TF_TEAM_COUNT]; + +EHANDLE g_hTestSub; + +ConVar tf_playerstatetransitions( "tf_playerstatetransitions", "-2", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "tf_playerstatetransitions <ent index or -1 for all>. Show player state transitions." ); +ConVar tf_playergib( "tf_playergib", "1", FCVAR_NOTIFY, "Allow player gibbing. 0: never, 1: normal, 2: always", true, 0, true, 2 ); + +ConVar tf_damageforcescale_other( "tf_damageforcescale_other", "6.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_damageforcescale_self_soldier_rj( "tf_damageforcescale_self_soldier_rj", "10.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_damageforcescale_self_soldier_badrj( "tf_damageforcescale_self_soldier_badrj", "5.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_damageforcescale_pyro_jump( "tf_damageforcescale_pyro_jump", "8.5", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_damagescale_self_soldier( "tf_damagescale_self_soldier", "0.60", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); + + +ConVar tf_damage_range( "tf_damage_range", "0.5", FCVAR_DEVELOPMENTONLY ); +ConVar tf_damage_multiplier_blue( "tf_damage_multiplier_blue", "1.0", FCVAR_CHEAT, "All incoming damage to a blue player is multiplied by this value" ); +ConVar tf_damage_multiplier_red( "tf_damage_multiplier_red", "1.0", FCVAR_CHEAT, "All incoming damage to a red player is multiplied by this value" ); + + +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" ); + +ConVar tf_allow_player_use( "tf_allow_player_use", "0", FCVAR_NOTIFY, "Allow players to execute +use while playing." ); + +ConVar tf_deploying_bomb_time( "tf_deploying_bomb_time", "1.90", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Time to deploy bomb before the point of no return." ); +ConVar tf_deploying_bomb_delay_time( "tf_deploying_bomb_delay_time", "0.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Time to delay before deploying bomb." ); + +#ifdef TF_RAID_MODE +ConVar tf_raid_team_size( "tf_raid_team_size", "5", FCVAR_NOTIFY, "Max number of Raiders" ); +ConVar tf_raid_respawn_safety_time( "tf_raid_respawn_safety_time", "1.5", FCVAR_NOTIFY, "Number of seconds of invulnerability after respawning" ); +ConVar tf_raid_allow_class_change( "tf_raid_allow_class_change", "1", FCVAR_NOTIFY, "If nonzero, allow invaders to change their class after leaving the safe room" ); +ConVar tf_raid_use_rescue_closets( "tf_raid_use_rescue_closets", "1", FCVAR_NOTIFY ); +ConVar tf_raid_drop_healthkit_chance( "tf_raid_drop_healthkit_chance", "50" ); // , FCVAR_CHEAT ); + +ConVar tf_boss_battle_team_size( "tf_boss_battle_team_size", "5", FCVAR_NOTIFY, "Max number of players in Boss Battle mode" ); +ConVar tf_boss_battle_respawn_safety_time( "tf_boss_battle_respawn_safety_time", "3", FCVAR_NOTIFY, "Number of seconds of invulnerability after respawning" ); +ConVar tf_boss_battle_respawn_on_friends( "tf_boss_battle_respawn_on_friends", "1", FCVAR_NOTIFY ); +#endif + +ConVar tf_mvm_death_penalty( "tf_mvm_death_penalty", "0", FCVAR_NOTIFY | FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "How much currency players lose when dying" ); +extern ConVar tf_populator_damage_multiplier; +extern ConVar tf_mvm_skill; + +#ifdef STAGING_ONLY +ConVar tf_debug_ballistics( "tf_debug_ballistics", "0", FCVAR_CHEAT ); +ConVar tf_debug_ballistic_targeting( "tf_debug_ballistic_targeting", "0", FCVAR_CHEAT ); +ConVar tf_debug_ballistic_targeting_tolerance( "tf_debug_ballistic_targeting_tolerance", "5", FCVAR_CHEAT ); +static Vector tf_debug_ballistic_target( 0, 0, 0 ); + +ConVar tf_space_thrust_scout( "tf_space_thrust_scout", "40.0", FCVAR_CHEAT | FCVAR_REPLICATED, "How much thrust is added while holding jump" ); +ConVar tf_space_thrust_sniper( "tf_space_thrust_sniper", "34.0", FCVAR_CHEAT | FCVAR_REPLICATED, "How much thrust is added while holding jump" ); +ConVar tf_space_thrust_spy( "tf_space_thrust_spy", "35.0", FCVAR_CHEAT | FCVAR_REPLICATED, "How much thrust is added while holding jump" ); +ConVar tf_space_thrust_pyro( "tf_space_thrust_pyro", "35.0", FCVAR_CHEAT | FCVAR_REPLICATED, "How much thrust is added while holding jump" ); +ConVar tf_space_thrust_soldier( "tf_space_thrust_soldier", "33.0", FCVAR_CHEAT | FCVAR_REPLICATED, "How much thrust is added while holding jump" ); +ConVar tf_space_thrust_engy( "tf_space_thrust_engy", "35.0", FCVAR_CHEAT | FCVAR_REPLICATED, "How much thrust is added while holding jump" ); +ConVar tf_space_thrust_medic( "tf_space_thrust_medic", "37.0", FCVAR_CHEAT | FCVAR_REPLICATED, "How much thrust is added while holding jump" ); +ConVar tf_space_thrust_heavy( "tf_space_thrust_heavy", "33.0", FCVAR_CHEAT | FCVAR_REPLICATED, "How much thrust is added while holding jump" ); +ConVar tf_space_thrust_demo( "tf_space_thrust_demo", "33.0", FCVAR_CHEAT | FCVAR_REPLICATED, "How much thrust is added while holding jump" ); +ConVar tf_space_thrust_use_rate( "tf_space_thrust_use_rate", "2.0", FCVAR_CHEAT | FCVAR_REPLICATED, "How much fuel is used per tick" ); +ConVar tf_space_thrust_recharge_rate( "tf_space_thrust_recharge_rate", "0.5", FCVAR_CHEAT | FCVAR_REPLICATED, "How much fuel is recharged per tick" ); +ConVar tf_skip_intro_and_spectate( "tf_skip_intro_and_spectate", "0", FCVAR_REPLICATED, "Skip intro panels and start spectating." ); +#endif + +#ifdef STAGING_ONLY +ConVar tf_highfive_separation_forward( "tf_highfive_separation_forward", "0", FCVAR_CHEAT, "Forward distance between high five partners" ); +ConVar tf_highfive_separation_right( "tf_highfive_separation_right", "0", FCVAR_CHEAT, "Right distance between high five partners" ); +#else +ConVar tf_highfive_separation_forward( "tf_highfive_separation_forward", "0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Forward distance between high five partners" ); +ConVar tf_highfive_separation_right( "tf_highfive_separation_right", "0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Right distance between high five partners" ); +#endif + +ConVar tf_highfive_max_range( "tf_highfive_max_range", "150", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "The farthest away a high five partner can be" ); +ConVar tf_highfive_height_tolerance( "tf_highfive_height_tolerance", "12", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "The maximum height difference allowed for two high-fivers." ); +ConVar tf_highfive_debug( "tf_highfive_debug", "0", FCVAR_NONE, "Turns on some console spew for debugging high five issues." ); + +ConVar tf_test_teleport_home_fx( "tf_test_teleport_home_fx", "0", FCVAR_CHEAT ); + +ConVar tf_halloween_giant_health_scale( "tf_halloween_giant_health_scale", "10", FCVAR_CHEAT ); + +ConVar tf_grapplinghook_los_force_detach_time( "tf_grapplinghook_los_force_detach_time", "1", FCVAR_CHEAT ); +ConVar tf_powerup_max_charge_time( "tf_powerup_max_charge_time", "30", FCVAR_CHEAT ); + +extern ConVar tf_powerup_mode; +extern ConVar tf_mvm_buybacks_method; +extern ConVar tf_mvm_buybacks_per_wave; + +#define TF_CANNONBALL_FORCE_SCALE 80.f +#define TF_CANNONBALL_FORCE_UPWARD 300.f + +#ifdef STAGING_ONLY +void CC_tf_debug_ballistic_targeting_mark_target( const CCommand &args ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( !player ) + { + return; + } + + Vector forward; + AngleVectors( player->EyeAngles() + player->GetPunchAngle(), &forward ); + + trace_t result; + UTIL_TraceLine( player->EyePosition(), player->EyePosition() + 2000.0f * forward, MASK_SHOT, player, COLLISION_GROUP_NONE, &result ); + + tf_debug_ballistic_target = result.endpos; +} +static ConCommand tf_debug_ballistic_targeting_mark_target( "tf_debug_ballistic_targeting_mark_target", CC_tf_debug_ballistic_targeting_mark_target, "Mark a spot for testing ballistic targeting.", FCVAR_CHEAT ); + +ConVar tf_infinite_ammo( "tf_infinite_ammo", "0", FCVAR_CHEAT ); + +extern ConVar tf_bountymode_currency_starting; +extern ConVar tf_bountymode_upgrades_wipeondeath; +extern ConVar tf_bountymode_currency_penalty_ondeath; +#endif // STAGING_ONLY + +ConVar tf_halloween_unlimited_spells( "tf_halloween_unlimited_spells", "0", FCVAR_CHEAT ); +extern ConVar tf_halloween_kart_boost_recharge; +extern ConVar tf_halloween_kart_boost_duration; + +ConVar tf_halloween_kart_impact_force( "tf_halloween_kart_impact_force", "0.75f", FCVAR_CHEAT, "Impact force scaler" ); +ConVar tf_halloween_kart_impact_damage( "tf_halloween_kart_impact_damage", "1.0f", FCVAR_CHEAT, "Impact damage scaler" ); +ConVar tf_halloween_kart_impact_rate( "tf_halloween_kart_impact_rate", "0.5f", FCVAR_CHEAT, "rate of allowing impact damage" ); +ConVar tf_halloween_kart_boost_impact_force( "tf_halloween_kart_boost_impact_force", "0.75f", FCVAR_CHEAT, "Impact force scaler on boosts" ); +ConVar tf_halloween_kart_impact_bounds_scale( "tf_halloween_kart_impact_bounds_scale", "1.0f", FCVAR_CHEAT ); +ConVar tf_halloween_kart_impact_feedback( "tf_halloween_kart_impact_feedback", "0.25f", FCVAR_CHEAT ); +ConVar tf_halloween_kart_impact_lookahead( "tf_halloween_kart_impact_lookahead", "12.0f", FCVAR_CHEAT ); +ConVar tf_halloween_kart_bomb_head_damage_scale( "tf_halloween_kart_bomb_head_damage_scale", "2", FCVAR_CHEAT ); +ConVar tf_halloween_kart_bomb_head_impulse_scale( "tf_halloween_kart_bomb_head_impulse_scale", "2", FCVAR_CHEAT ); +ConVar tf_halloween_kart_impact_air_scale( "tf_halloween_kart_impact_air_scale", "0.75f", FCVAR_CHEAT ); +ConVar tf_halloween_kart_damage_to_force( "tf_halloween_kart_damage_to_force", "300.0f", FCVAR_CHEAT ); +ConVar tf_halloween_kart_stun_duration_scale( "tf_halloween_kart_stun_duration_scale", "0.70f", FCVAR_CHEAT ); +ConVar tf_halloween_kart_stun_amount( "tf_halloween_kart_stun_amount", "1.0f", FCVAR_CHEAT ); +ConVar tf_halloween_kart_stun_enabled( "tf_halloween_kart_stun_enabled", "1", FCVAR_CHEAT ); + +ConVar tf_tauntcam_fov_override( "tf_tauntcam_fov_override", "0", FCVAR_CHEAT ); + +ConVar tf_nav_in_combat_range( "tf_nav_in_combat_range", "1000", FCVAR_CHEAT ); + +ConVar tf_halloween_kart_punting_ghost_force_scale( "tf_halloween_kart_punting_ghost_force_scale", "4", FCVAR_CHEAT ); +ConVar tf_halloween_allow_ghost_hit_by_kart_delay( "tf_halloween_allow_ghost_hit_by_kart_delay", "0.5", FCVAR_CHEAT ); + +extern ConVar tf_feign_death_duration; +extern ConVar spec_freeze_time; +extern ConVar spec_freeze_traveltime; +extern ConVar sv_maxunlag; +extern ConVar tf_allow_taunt_switch; +extern ConVar weapon_medigun_chargerelease_rate; +extern ConVar tf_scout_energydrink_consume_rate; +extern ConVar tf_mm_trusted; +extern ConVar mp_spectators_restricted; +extern ConVar mp_teams_unbalance_limit; +extern ConVar tf_tournament_classchange_allowed; +extern ConVar tf_tournament_classchange_ready_allowed; +#if defined( _DEBUG ) || defined( STAGING_ONLY ) +extern ConVar mp_developer; +#endif // _DEBUG || STAGING_ONLY +#ifdef STAGING_ONLY +extern ConVar tf_skillrating_debug_bots_allowed; +#endif // STAGING_ONLY + +extern CBaseEntity *FindPickerEntity( CBasePlayer *pPlayer ); +extern bool CanScatterGunKnockBack( CTFWeaponBase *pWeapon, float flDamage, float flDistanceSq ); + +static const char *s_pszTauntRPSParticleNames[] = +{ + "rps_rock_red", + "rps_paper_red", + "rps_scissors_red", + "rps_rock_red_win", + "rps_paper_red_win", + "rps_scissors_red_win", + "rps_rock_blue", + "rps_paper_blue", + "rps_scissors_blue", + "rps_rock_blue_win", + "rps_paper_blue_win", + "rps_scissors_blue_win" +}; + +// -------------------------------------------------------------------------------- // +// 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 ) ) + { + // if prediction is off, alway send jump + if ( !( ( event == PLAYERANIMEVENT_JUMP ) && ( FStrEq(engine->GetClientConVarValue( pPlayer->entindex(), "cl_predict" ), "0" ) ) ) ) + { + filter.RemoveRecipient( pPlayer ); + } + } + + Assert( pPlayer->entindex() >= 1 && pPlayer->entindex() <= MAX_PLAYERS ); + g_TEPlayerAnimEvent.m_iPlayerIndex = pPlayer->entindex(); + g_TEPlayerAnimEvent.m_iEvent = event; + Assert( nData < (1<<ANIMATION_SEQUENCE_BITS) ); + Assert( (1<<ANIMATION_SEQUENCE_BITS) >= ActivityList_HighestIndex() ); + g_TEPlayerAnimEvent.m_nData = nData; + g_TEPlayerAnimEvent.Create( filter, 0 ); +} + +//================================================================================= +// +// Ragdoll Entity +// +class CTFRagdoll : public CBaseAnimatingOverlay +{ +public: + + DECLARE_CLASS( CTFRagdoll, CBaseAnimatingOverlay ); + DECLARE_SERVERCLASS(); + + CTFRagdoll() + { + m_iPlayerIndex.Set( TF_PLAYER_INDEX_NONE ); + m_bGib = false; + m_bBurning = false; + m_bElectrocuted = false; + m_bFeignDeath = false; + m_bWasDisguised = false; + m_bBecomeAsh = false; + m_bOnGround = false; + m_bCloaked = false; + m_iDamageCustom = 0; + m_bCritOnHardHit = false; + m_vecRagdollOrigin.Init(); + m_vecRagdollVelocity.Init(); + } + + ~CTFRagdoll() + { + // Destroy all of our attached wearables. + for ( int i=0; i<m_hRagWearables.Count(); ++i ) + { + if ( m_hRagWearables[i] ) + { + m_hRagWearables[i]->Remove(); + } + } + m_hRagWearables.Purge(); + } + + // Transmit ragdolls to everyone. + virtual int UpdateTransmitState() + { + UseClientSideAnimation(); + return SetTransmitState( FL_EDICT_ALWAYS ); + } + + CNetworkVar( int, m_iPlayerIndex ); + CNetworkVector( m_vecRagdollVelocity ); + CNetworkVector( m_vecRagdollOrigin ); + CNetworkVar( bool, m_bGib ); + CNetworkVar( bool, m_bBurning ); + CNetworkVar( bool, m_bElectrocuted ); + CNetworkVar( bool, m_bFeignDeath ); + CNetworkVar( bool, m_bWasDisguised ); + CNetworkVar( bool, m_bBecomeAsh ); + CNetworkVar( bool, m_bOnGround ); + CNetworkVar( bool, m_bCloaked ); + CNetworkVar( int, m_iDamageCustom ); + CNetworkVar( int, m_iTeam ); + CNetworkVar( int, m_iClass ); + CNetworkVar( bool, m_bGoldRagdoll ); + CNetworkVar( bool, m_bIceRagdoll ); + CNetworkVar( bool, m_bCritOnHardHit ); + CNetworkVar( float, m_flHeadScale ); + CNetworkVar( float, m_flTorsoScale ); + CNetworkVar( float, m_flHandScale ); + CUtlVector<CHandle<CEconWearable > > m_hRagWearables; +}; + +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 ) ), + SendPropBool( SENDINFO( m_bElectrocuted ) ), + SendPropBool( SENDINFO( m_bFeignDeath ) ), + SendPropBool( SENDINFO( m_bWasDisguised ) ), + SendPropBool( SENDINFO( m_bBecomeAsh ) ), + SendPropBool( SENDINFO( m_bOnGround ) ), + SendPropBool( SENDINFO( m_bCloaked ) ), + SendPropInt( SENDINFO( m_iDamageCustom ) ), + SendPropInt( SENDINFO( m_iTeam ), 3, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iClass ), 4, SPROP_UNSIGNED ), + SendPropUtlVector( SENDINFO_UTLVECTOR( m_hRagWearables ), 8, SendPropEHandle( NULL, 0 ) ), + SendPropBool( SENDINFO( m_bGoldRagdoll ) ), + SendPropBool( SENDINFO( m_bIceRagdoll ) ), + SendPropBool( SENDINFO( m_bCritOnHardHit ) ), + SendPropFloat( SENDINFO( m_flHeadScale ) ), + SendPropFloat( SENDINFO( m_flTorsoScale ) ), + SendPropFloat( SENDINFO( m_flHandScale ) ), +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 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int SendProxyArrayLength_PlayerObjects( const void *pStruct, int objectID ) +{ + CTFPlayer *pPlayer = (CTFPlayer*)pStruct; + int iObjects = pPlayer->GetObjectCount(); + Assert( iObjects <= MAX_OBJECTS_PER_PLAYER ); + return iObjects; +} + +//----------------------------------------------------------------------------- +// Purpose: Send to attached medics +//----------------------------------------------------------------------------- +void* SendProxy_SendHealersDataTable( const SendProp *pProp, const void *pStruct, const void *pVarData, CSendProxyRecipients *pRecipients, int objectID ) +{ + CTFPlayer *pPlayer = (CTFPlayer*)pStruct; + if ( pPlayer ) + { + // Add attached medics + for ( int i = 0; i < pPlayer->m_Shared.GetNumHealers(); i++ ) + { + CTFPlayer *pMedic = ToTFPlayer( pPlayer->m_Shared.GetHealerByIndex( i ) ); + if ( !pMedic ) + continue; + + pRecipients->SetRecipient( pMedic->GetClientIndex() ); + return (void*)pVarData; + } + } + + return NULL; +} +REGISTER_SEND_PROXY_NON_MODIFIED_POINTER( SendProxy_SendHealersDataTable ); + +BEGIN_DATADESC( CTFPlayer ) + DEFINE_INPUTFUNC( FIELD_VOID, "IgnitePlayer", InputIgnitePlayer ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetCustomModel", InputSetCustomModel ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetCustomModelOffset", InputSetCustomModelOffset ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetCustomModelRotation", InputSetCustomModelRotation ), + DEFINE_INPUTFUNC( FIELD_VOID, "ClearCustomModelRotation", InputClearCustomModelRotation ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetCustomModelRotates", InputSetCustomModelRotates ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetCustomModelVisibleToSelf", InputSetCustomModelVisibleToSelf ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetForcedTauntCam", InputSetForcedTauntCam ), + DEFINE_INPUTFUNC( FIELD_VOID, "ExtinguishPlayer", InputExtinguishPlayer ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "BleedPlayer", InputBleedPlayer ), + DEFINE_INPUTFUNC( FIELD_VOID, "TriggerLootIslandAchievement", InputTriggerLootIslandAchievement ), + DEFINE_INPUTFUNC( FIELD_VOID, "TriggerLootIslandAchievement2", InputTriggerLootIslandAchievement2 ), + DEFINE_INPUTFUNC( FIELD_STRING, "SpeakResponseConcept", InputSpeakResponseConcept ), + DEFINE_INPUTFUNC( FIELD_VOID, "RollRareSpell", InputRollRareSpell ), + DEFINE_INPUTFUNC( FIELD_VOID, "RoundSpawn", InputRoundSpawn ), +END_DATADESC() + +EXTERN_SEND_TABLE( DT_ScriptCreatedItem ); + +// 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 + SendPropVectorXY(SENDINFO(m_vecOrigin), -1, SPROP_NOSCALE|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_OriginXY ), + SendPropFloat (SENDINFO_VECTORELEM(m_vecOrigin, 2), -1, SPROP_NOSCALE|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_OriginZ ), + 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 ), // No longer used by the local player, could be omitted. Preserved for backwards-compat for now. +// SendPropAngle( SENDINFO_VECTORELEM(m_angEyeAngles, 1), 10, SPROP_CHANGES_OFTEN ), + + SendPropBool( SENDINFO( m_bIsCoaching ) ), + SendPropEHandle( SENDINFO( m_hCoach ) ), + SendPropEHandle( SENDINFO( m_hStudent ) ), + + SendPropInt( SENDINFO( m_nCurrency ), -1, SPROP_VARINT ), + SendPropInt( SENDINFO( m_nExperienceLevel ), 7, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_nExperienceLevelProgress ), 7, SPROP_UNSIGNED ), + SendPropBool( SENDINFO( m_bMatchSafeToLeave ) ), + +END_SEND_TABLE() + +// all players except the local player +BEGIN_SEND_TABLE_NOBASE( CTFPlayer, DT_TFNonLocalPlayerExclusive ) + // send a lo-res origin to other players + SendPropVectorXY(SENDINFO(m_vecOrigin), -1, SPROP_COORD_MP_LOWPRECISION|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_OriginXY ), + SendPropFloat (SENDINFO_VECTORELEM(m_vecOrigin, 2), -1, SPROP_COORD_MP_LOWPRECISION|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_OriginZ ), + + 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() + +//----------------------------------------------------------------------------- +// Purpose: Sent to attached medics +//----------------------------------------------------------------------------- +BEGIN_SEND_TABLE_NOBASE( CTFPlayer, DT_TFSendHealersDataTable ) + SendPropInt( SENDINFO( m_nActiveWpnClip ), -1, SPROP_VARINT | SPROP_UNSIGNED ), +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_BaseAnimating", "m_nBody" ), + 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)), + SendPropBool(SENDINFO(m_bIsMiniBoss)), + SendPropBool(SENDINFO(m_bIsABot)), + SendPropInt( SENDINFO(m_nBotSkill), 3, SPROP_UNSIGNED ), + + // 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 ), + + // 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 ) ), + SendPropEHandle(SENDINFO(m_hItem)), + + // 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_bAllowMoveDuringTaunt ) ), + SendPropBool( SENDINFO( m_bIsReadyToHighFive ) ), + SendPropEHandle( SENDINFO( m_hHighFivePartner ) ), + SendPropInt( SENDINFO( m_nForceTauntCam ), 2, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO( m_flTauntYaw ), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO( m_nActiveTauntSlot ) ), + SendPropInt( SENDINFO( m_iTauntItemDefIndex ) ), + SendPropFloat( SENDINFO( m_flCurrentTauntMoveSpeed ) ), + SendPropFloat( SENDINFO( m_flVehicleReverseTime ) ), + + SendPropFloat( SENDINFO( m_flLastDamageTime ), 16, SPROP_ROUNDUP ), + + SendPropBool( SENDINFO( m_bInPowerPlay ) ), + + SendPropInt( SENDINFO( m_iSpawnCounter ) ), + SendPropBool( SENDINFO( m_bArenaSpectator ) ), + SendPropFloat( SENDINFO( m_flHeadScale ) ), + SendPropFloat( SENDINFO( m_flTorsoScale ) ), + SendPropFloat( SENDINFO( m_flHandScale ) ), + + SendPropBool( SENDINFO( m_bUseBossHealthBar ) ), + + SendPropBool( SENDINFO( m_bUsingVRHeadset ) ), + + SendPropBool( SENDINFO( m_bForcedSkin ) ), + SendPropInt( SENDINFO( m_nForcedSkin ), ANIMATION_SKIN_BITS ), + + SendPropDataTable( SENDINFO_DT( m_AttributeManager ), &REFERENCE_SEND_TABLE(DT_AttributeManager) ), + + SendPropDataTable( "TFSendHealersDataTable", 0, &REFERENCE_SEND_TABLE( DT_TFSendHealersDataTable ), SendProxy_SendHealersDataTable ), + + SendPropFloat( SENDINFO( m_flKartNextAvailableBoost ) ), + SendPropInt( SENDINFO( m_iKartHealth ) ), + SendPropInt( SENDINFO( m_iKartState ) ), + SendPropEHandle( SENDINFO( m_hGrapplingHookTarget ) ), + SendPropEHandle( SENDINFO( m_hSecondaryLastWeapon ) ), + SendPropBool( SENDINFO( m_bUsingActionSlot ) ), + SendPropFloat( SENDINFO( m_flInspectTime ) ), + SendPropInt( SENDINFO( m_iCampaignMedals ) ), + SendPropInt( SENDINFO( m_iPlayerSkinOverride ) ), +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 ); + +// -------------------------------------------------------------------------------- // + +enum eCoachCommand +{ + kCoachCommand_Look = 1, // slot1 + kCoachCommand_Go, // slot2 + kCoachCommand_Attack, + kCoachCommand_Defend, + kNumCoachCommands, +}; + +/** + * Handles a command from the coach + */ +static void HandleCoachCommand( CTFPlayer *pPlayer, eCoachCommand command ) +{ + if ( pPlayer && pPlayer->IsCoaching() && pPlayer->GetStudent() && command < kNumCoachCommands ) + { + const float kMaxRateCoachCommands = 1.0f; + float flLastCoachCommandDelta = gpGlobals->curtime - pPlayer->m_flLastCoachCommand; + if ( flLastCoachCommandDelta < kMaxRateCoachCommands && flLastCoachCommandDelta > 0.0f ) + { + return; + } + pPlayer->m_flLastCoachCommand = gpGlobals->curtime; + IGameEvent *pEvent = gameeventmanager->CreateEvent( "show_annotation" ); + if ( pEvent ) + { + Vector vForward; + AngleVectors( pPlayer->EyeAngles(), &vForward ); + + trace_t trace; + CTraceFilterSimple filter( pPlayer->GetStudent(), COLLISION_GROUP_NONE ); + UTIL_TraceLine( pPlayer->EyePosition(), pPlayer->EyePosition() + vForward * MAX_TRACE_LENGTH, MASK_SOLID, &filter, &trace ); + + CBaseEntity *pHitEntity = trace.m_pEnt && trace.m_pEnt->IsWorld() == false && trace.m_pEnt != pPlayer->GetStudent() ? trace.m_pEnt : NULL; + pEvent->SetInt( "id", pPlayer->entindex() ); + pEvent->SetFloat( "worldPosX", trace.endpos.x ); + pEvent->SetFloat( "worldPosY", trace.endpos.y ); + pEvent->SetFloat( "worldPosZ", trace.endpos.z ); + pEvent->SetFloat( "worldNormalX", trace.plane.normal.x ); + pEvent->SetFloat( "worldNormalY", trace.plane.normal.y ); + pEvent->SetFloat( "worldNormalZ", trace.plane.normal.z ); + pEvent->SetFloat( "lifetime", 10.0f ); + if ( pHitEntity ) + { + pEvent->SetInt( "follow_entindex", pHitEntity->entindex() ); + } + pEvent->SetInt( "visibilityBitfield", ( 1 << pPlayer->entindex() | 1 << pPlayer->GetStudent()->entindex() ) ); + pEvent->SetBool( "show_distance", true ); + pEvent->SetBool( "show_effect", true ); + + switch ( command ) + { + case kCoachCommand_Attack: + pEvent->SetString( "text", pHitEntity ? "#TF_Coach_AttackThis" : "#TF_Coach_AttackHere" ); + pEvent->SetString( "play_sound", "coach/coach_attack_here.wav" ); + break; + case kCoachCommand_Defend: + pEvent->SetString( "text", pHitEntity ? "#TF_Coach_DefendThis" : "#TF_Coach_DefendHere" ); + pEvent->SetString( "play_sound", "coach/coach_defend_here.wav" ); + break; + case kCoachCommand_Look: + pEvent->SetString( "text", pHitEntity ? "#TF_Coach_LookAt" : "#TF_Coach_LookHere" ); + pEvent->SetString( "play_sound", "coach/coach_look_here.wav" ); + break; + case kCoachCommand_Go: + pEvent->SetString( "text", pHitEntity ? "#TF_Coach_GoToThis" : "#TF_Coach_GoHere" ); + pEvent->SetString( "play_sound", "coach/coach_go_here.wav" ); + break; + } + gameeventmanager->FireEvent( pEvent ); + } + + } +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTFPlayer::CTFPlayer() +{ + m_pAttributes = this; + + m_PlayerAnimState = CreateTFPlayerAnimState( this ); + + SetArmorValue( 10 ); + + m_hItem = NULL; + m_hTauntScene = NULL; + m_hTauntProp = NULL; + + UseClientSideAnimation(); + m_angEyeAngles.Init(); + m_pStateInfo = NULL; + m_lifeState = LIFE_DEAD; // Start "dead". + m_iMaxSentryKills = 0; + m_flLastCoachCommand = 0; + + m_flNextTimeCheck = gpGlobals->curtime; + m_flSpawnTime = 0; + + m_flWaterExitTime = 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" ); + + m_flLastAction = gpGlobals->curtime; + m_flTimeInSpawn = 0; + + m_bInitTaunt = false; + + m_bSpeakingConceptAsDisguisedSpy = false; + + m_iPreviousteam = TEAM_UNASSIGNED; + m_bArenaSpectator = false; + + m_bArenaIsAFK = false; + m_bIsAFK = false; + + m_nDeployingBombState = TF_BOMB_DEPLOYING_NONE; + + m_flNextChangeClassTime = 0.0f; + m_flNextChangeTeamTime = 0.0f; + + m_bScattergunJump = false; + m_iOldStunFlags = 0; + m_iLastWeaponSlot = 1; + m_iNumberofDominations = 0; + m_bFlipViewModels = false; + m_iBlastJumpState = 0; + m_flBlastJumpLandTime = 0; + m_fMaxHealthTime = -1; + m_iHealthBefore = 0; + + m_iTeamChanges = 0; + m_iClassChanges = 0; + + m_hReviveMarker = NULL; + + // Bounty Mode + m_nExperienceLevel = 1; + m_nExperiencePoints = 0; + m_nExperienceLevelProgress = 0; + + SetDefLessFunc( m_Cappers ); // Tracks victims for demo achievement + + //============================================================================= + // HPE_BEGIN: + // [msmith] Added a player type so we can distinguish between bots and humans. + //============================================================================= + m_playerType = HUMAN_PLAYER; + //============================================================================= + // HPE_END + //============================================================================= + + m_bIsTargetDummy = false; + + m_bCollideWithSentry = false; + + m_flCommentOnCarrying = 0; + + m_bIsReadyToHighFive = false; + m_hHighFivePartner = NULL; + m_nForceTauntCam = 0; + m_bAllowMoveDuringTaunt = false; + m_bTauntForceMoveForward = false; + m_flTauntForceMoveForwardSpeed = 0.f; + m_flTauntMoveAccelerationTime = 0.f; + m_flTauntTurnSpeed = 0.f; + m_flTauntTurnAccelerationTime = 0.f; + m_bTauntMimic = false; + m_bIsTauntInitiator = false; + m_TauntEconItemView.Invalidate(); + m_iPreTauntWeaponSlot = -1; + + m_bIsCalculatingMaximumSpeed = false; + + m_flLastThinkTime = -1.f; + + m_nCurrency = 0; + m_pWaveSpawnPopulator = NULL; + m_flLastReadySoundTime = 0.f; + + m_damageRateArray = new int[ DPS_Period ]; + ResetDamagePerSecond(); + + m_nActiveWpnClip.Set( 0 ); + m_nActiveWpnClipPrev = 0; + m_flNextClipSendTime = 0; + + m_nCanPurchaseUpgradesCount = 0; + + m_flHeadScale = 1.f; + m_flTorsoScale = 1.f; + m_flHandScale = 1.f; + + m_bPendingMerasmusPlayerBombExplode = false; + m_fLastBombHeadTimestamp = 0.0f; + + m_bIsSapping = false; + m_iSappingEvent = TF_SAPEVENT_NONE; + m_flSapStartTime = 0.00; + + m_bIsMiniBoss = false; + + m_bUseBossHealthBar = false; + + m_bUsingVRHeadset = false; + + m_bForcedSkin = false; + m_nForcedSkin = 0; + + SetRespawnOverride( -1.f, NULL_STRING ); + + m_qPreviousChargeEyeAngle.Init(); + + m_vHalloweenKartPush.Zero(); + m_flHalloweenKartPushEventTime = 0.f; + m_bCheckKartCollision = false; + m_flHHHKartAttackTime = 0.f; + m_flNextBonusDucksVOAllowedTime = 0.f; + + m_flGhostLastHitByKartTime = 0.f; + + m_flVehicleReverseTime = FLT_MAX; + m_iCampaignMedals = 0; + + m_bPasstimeBallSlippery = false; + m_flNextScorePointForPD = -1; + + m_iPlayerSkinOverride = 0; + + m_nPrevRoundTeamNum = TEAM_UNASSIGNED; + m_flLastDamageResistSoundTime = -1.f; + m_hLastDamageDoneEntity = NULL; + + m_mapCustomAttributes.SetLessFunc( UtlStringCaseInsensitiveLessFunc ); + + SetDefLessFunc( m_PlayersExtinguished ); + + m_flLastAutobalanceTime = 0.f; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ForcePlayerViewAngles( const QAngle& qTeleportAngles ) +{ + CSingleUserRecipientFilter filter( this ); + + UserMessageBegin( filter, "ForcePlayerViewAngles" ); + WRITE_BYTE( 0x01 ); // Reserved space for flags. + WRITE_BYTE( entindex() ); + WRITE_ANGLES( qTeleportAngles ); + MessageEnd(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::SetGrapplingHookTarget( CBaseEntity *pTarget, bool bShouldBleed /*= false*/ ) +{ + if ( pTarget ) + { + // prevent fall damage after a successful hook + m_Shared.AddCond( TF_COND_GRAPPLINGHOOK_SAFEFALL ); + m_Shared.AddCond( TF_COND_GRAPPLINGHOOK_LATCHED ); + } + else + { + m_Shared.RemoveCond( TF_COND_GRAPPLINGHOOK_LATCHED ); + } + + CBaseEntity *pPreviousTarget = m_hGrapplingHookTarget; + m_hGrapplingHookTarget = pTarget; + + if ( pTarget ) + { + if ( pTarget->IsPlayer() ) + { + CTFPlayer *pTargetPlayer = ToTFPlayer( pTarget ); + + m_Shared.AddCond( TF_COND_GRAPPLED_TO_PLAYER ); + + // make player bleed + if ( bShouldBleed ) + { + CTFGrapplingHook *pGrapplingHook = dynamic_cast< CTFGrapplingHook* >( GetEntityForLoadoutSlot( LOADOUT_POSITION_ACTION ) ); + if ( pGrapplingHook ) + pTargetPlayer->m_Shared.MakeBleed( this, pGrapplingHook, 0, TF_BLEEDING_DMG, true ); + + pTargetPlayer->m_nHookAttachedPlayers++; + } + + if ( !pTargetPlayer->m_Shared.InCond( TF_COND_GRAPPLINGHOOK_BLEEDING ) && pTargetPlayer->m_nHookAttachedPlayers > 0 ) + { + pTargetPlayer->m_Shared.AddCond( TF_COND_GRAPPLINGHOOK_BLEEDING ); + } + if ( !pTargetPlayer->m_Shared.InCond( TF_COND_GRAPPLED_BY_PLAYER ) && pTargetPlayer->m_nHookAttachedPlayers > 0 ) + { + pTargetPlayer->m_Shared.AddCond( TF_COND_GRAPPLED_BY_PLAYER ); + } + } + + m_flLastSeenHookTarget = gpGlobals->curtime; + } + else + { + if ( pPreviousTarget && pPreviousTarget->IsPlayer() ) + { + CTFPlayer *pPreviousTargetPlayer = ToTFPlayer( pPreviousTarget ); + + m_Shared.RemoveCond( TF_COND_GRAPPLED_TO_PLAYER ); + + // try to remove bleeding from hook if there's one + if ( pPreviousTargetPlayer->m_Shared.InCond( TF_COND_BLEEDING ) ) + { + CTFGrapplingHook *pGrapplingHook = dynamic_cast< CTFGrapplingHook* >( GetEntityForLoadoutSlot( LOADOUT_POSITION_ACTION ) ); + if ( pGrapplingHook ) + pPreviousTargetPlayer->m_Shared.StopBleed( this, pGrapplingHook ); + } + + pPreviousTargetPlayer->m_nHookAttachedPlayers--; + Assert( pPreviousTargetPlayer->m_nHookAttachedPlayers >= 0 ); + if ( pPreviousTargetPlayer->m_nHookAttachedPlayers == 0 ) + { + pPreviousTargetPlayer->m_Shared.RemoveCond( TF_COND_GRAPPLINGHOOK_BLEEDING ); + pPreviousTargetPlayer->m_Shared.RemoveCond( TF_COND_GRAPPLED_BY_PLAYER ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::CanBeForcedToLaugh( void ) +{ + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && IsBot() && ( GetTeamNumber() == TF_TEAM_PVE_INVADERS ) ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::TFPlayerThink() +{ + if ( m_pStateInfo && m_pStateInfo->pfnThink ) + { + (this->*m_pStateInfo->pfnThink)(); + } + + if ( m_flSendPickupWeaponMessageTime != -1.f && gpGlobals->curtime >= m_flSendPickupWeaponMessageTime ) + { + CSingleUserRecipientFilter filter( this ); + filter.MakeReliable(); + UserMessageBegin( filter, "PlayerPickupWeapon" ); + MessageEnd(); + + m_flSendPickupWeaponMessageTime = -1.f; + } + + // In doomsday event, kart can run over ghost to do stuff + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TEAM_ANY, true ); + CUtlVector< CTFPlayer * > ghostVector; + for ( int i=0; i<playerVector.Count(); ++i ) + { + if ( playerVector[i] == this ) + continue; + + // touching ghost player? + // we just check for radius of 100 and assume that we touch to avoid custom collision for ghost + if ( playerVector[i]->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) ) + { + if ( ( playerVector[i]->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr() < Square( 100 ) ) + { + ghostVector.AddToTail( playerVector[i] ); + } + } + } + + for ( int i=0; i<ghostVector.Count(); ++i ) + { + CTFPlayer *pGhost = ghostVector[i]; + + // revive ghost on the same team + if ( pGhost->GetTeamNumber() == GetTeamNumber() ) + { + // Trace the ghosts bbox right where they are to see if they collide with enemy players + trace_t trace; + Ray_t ray; + ray.Init( pGhost->GetAbsOrigin(), pGhost->GetAbsOrigin(), pGhost->GetPlayerMins(), pGhost->GetPlayerMaxs() ); + UTIL_TraceRay( ray, PlayerSolidMask(), pGhost, COLLISION_GROUP_PLAYER, &trace ); + + // If our trace is clear, spawn that ghost + if ( trace.fraction == 1.0f ) + { + // Force the players kart angles to line up with our current ghost angles. + // This should put us in the kart at the same direction we are currently looking. + pGhost->ForcePlayerViewAngles( pGhost->GetAbsAngles() ); + + pGhost->m_Shared.RemoveCond( TF_COND_HALLOWEEN_GHOST_MODE ); + pGhost->m_Shared.AddCond( TF_COND_HALLOWEEN_KART ); + pGhost->m_Shared.AddCond( TF_COND_HALLOWEEN_IN_HELL ); // keep you in hell to be able to respawn as ghost + pGhost->m_Shared.AddCond( TF_COND_HALLOWEEN_QUICK_HEAL, 3, this ); + pGhost->EmitSound( "BumperCar.SpawnFromLava" ); + DispatchParticleEffect( "ghost_appearation", PATTACH_ABSORIGIN, pGhost ); + + if ( TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) ) + { + // achievement for me! + AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_DOOMSDAY_RESPAWN_TEAMMATES ); + + IGameEvent *pEvent = gameeventmanager->CreateEvent( "respawn_ghost" ); + if ( pEvent ) + { + pEvent->SetInt( "reviver", GetUserID() ); + pEvent->SetInt( "ghost", pGhost->GetUserID() ); + gameeventmanager->FireEvent( pEvent, true ); + } + } + } + } + else if ( tf_halloween_allow_ghost_hit_by_kart_delay.GetFloat() > 0 && gpGlobals->curtime - pGhost->m_flGhostLastHitByKartTime > tf_halloween_allow_ghost_hit_by_kart_delay.GetFloat() ) + { + // punt off other team ghost + float flImpactForce = GetLocalVelocity().Length(); + flImpactForce = MAX( 100.f, flImpactForce ); // add min force + Vector vOffset = pGhost->WorldSpaceCenter() - WorldSpaceCenter(); + vOffset.z = 0; + Vector vPuntDir = ( vOffset ).Normalized(); + vPuntDir.z = 0.5f; + pGhost->ApplyAirBlastImpulse( tf_halloween_kart_punting_ghost_force_scale.GetFloat() * flImpactForce * vPuntDir ); + pGhost->EmitSound( "BumperCar.HitGhost" ); + pGhost->m_flGhostLastHitByKartTime = gpGlobals->curtime; + } + } + } + + if ( TFGameRules() && TFGameRules()->IsUsingGrapplingHook() ) + { + if ( IsUsingActionSlot() && GetActiveTFWeapon() && GetActiveTFWeapon()->GetWeaponID() != TF_WEAPON_GRAPPLINGHOOK ) + { + CTFGrapplingHook *pGrapplingHook = dynamic_cast< CTFGrapplingHook* >( GetEntityForLoadoutSlot( LOADOUT_POSITION_ACTION ) ); + if ( pGrapplingHook ) + { + Weapon_Switch( pGrapplingHook ); + } + } + + CBaseEntity *pHookTarget = GetGrapplingHookTarget(); + if ( pHookTarget ) + { + // detatch hook if the object's picked up + if ( pHookTarget->IsBaseObject() ) + { + CBaseObject *pObj = assert_cast< CBaseObject* >( pHookTarget ); + if ( pObj->IsCarried() ) + { + SetGrapplingHookTarget( NULL ); + pHookTarget = NULL; + } + } + + // check if something is blocking the player from traveling to the hook target + if ( pHookTarget ) + { + trace_t tr; + CTraceFilterLOS filter( this, COLLISION_GROUP_PLAYER_MOVEMENT, pHookTarget ); + UTIL_TraceLine( WorldSpaceCenter(), pHookTarget->WorldSpaceCenter(), MASK_PLAYERSOLID, &filter, &tr ); + if ( !tr.DidHit() ) + { + m_flLastSeenHookTarget = gpGlobals->curtime; + } + else if ( gpGlobals->curtime - m_flLastSeenHookTarget > tf_grapplinghook_los_force_detach_time.GetFloat() ) + { + // force to detach if the hooker lost sight of the target for sometime + SetGrapplingHookTarget( NULL ); + } + } + } + } + + UpdateCustomAttributes(); + + // Time to finish the current random expression? Or time to pick a new one? + if ( IsAlive() && !IsReadyToTauntWithPartner() && ( m_flNextSpeakWeaponFire < gpGlobals->curtime ) && 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(); + } + + if ( IsTaunting() ) + { + if ( !m_strTauntSoundName.IsEmpty() && m_flTauntSoundTime > 0 && m_flTauntSoundTime <= gpGlobals->curtime ) + { + EmitSound( m_strTauntSoundName.String() ); + m_flTauntSoundTime = 0.f; + } + + if ( !m_strTauntSoundLoopName.IsEmpty() && m_flTauntSoundLoopTime > 0 && m_flTauntSoundLoopTime <= gpGlobals->curtime ) + { + CReliableBroadcastRecipientFilter filter; + UserMessageBegin( filter, "PlayerTauntSoundLoopStart" ); + WRITE_BYTE( entindex() ); + WRITE_STRING( m_strTauntSoundLoopName.String() ); + MessageEnd(); + + m_flTauntSoundLoopTime = 0.f; + } + + // play taunt outro + if ( m_flTauntOutroTime > 0.f && m_flTauntOutroTime <= gpGlobals->curtime ) + { + m_bAllowedToRemoveTaunt = true; + float flDuration = PlayTauntOutroScene(); + m_flTauntRemoveTime = gpGlobals->curtime + flDuration; + m_flTauntOutroTime = 0.f; + } + } + + // Halloween Hacks + // Spell Casting on Attack1 + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + // Check if this is the spellbook so we can save off info to preserve weapon switching + CTFSpellBook *pSpellBook = dynamic_cast<CTFSpellBook*>( GetEntityForLoadoutSlot( LOADOUT_POSITION_ACTION ) ); + if ( pSpellBook ) + { + // cast Spell + if ( m_nButtons & IN_ATTACK ) + { + if ( pSpellBook ) + { + pSpellBook->PrimaryAttack(); + } + } + } + + // Speed Boost + if ( m_nButtons & IN_ATTACK2 ) + { + if ( GetKartSpeedBoost() >= 1.0f ) + { + m_flKartNextAvailableBoost = gpGlobals->curtime + tf_halloween_kart_boost_recharge.GetFloat(); + m_Shared.AddCond( TF_COND_HALLOWEEN_KART_DASH, tf_halloween_kart_boost_duration.GetFloat() ); + } + } + } + + CBaseEntity *pGroundEntity = GetGroundEntity(); + + // We consider players "in air" if they have no ground entity and they're not in water. + if ( pGroundEntity == NULL && GetWaterLevel() == WL_NotInWater ) + { + if ( m_iLeftGroundHealth < 0 ) + { + m_iLeftGroundHealth = GetHealth(); + } + } + else + { + m_iLeftGroundHealth = -1; + if ( GetFlags() & FL_ONGROUND ) + { + m_Shared.RemoveCond( TF_COND_KNOCKED_INTO_AIR ); + } + + if ( m_iBlastJumpState ) + { + const char *pszEvent = NULL; + + if ( StickyJumped() ) + { + pszEvent = "sticky_jump_landed"; + } + else if ( RocketJumped() ) + { + pszEvent = "rocket_jump_landed"; + } + + ClearBlastJumpState(); + + if ( pszEvent ) + { + IGameEvent * event = gameeventmanager->CreateEvent( pszEvent ); + if ( event ) + { + event->SetInt( "userid", GetUserID() ); + gameeventmanager->FireEvent( event ); + } + } + } + } + + if( IsTaunting() ) + { + bool bStopTaunt = false; + // if I'm not supposed to move during taunt + // stop taunting if I lost my ground entity or was moved at all + if ( !CanMoveDuringTaunt() ) + { + bStopTaunt |= pGroundEntity == NULL; + + if ( m_TauntEconItemView.IsValid() && m_TauntEconItemView.GetStaticData()->GetTauntData()->ShouldStopTauntIfMoved() ) + bStopTaunt |= m_vecTauntStartPosition.DistToSqr( GetAbsOrigin() ) > 0.1f; + } + + if ( !bStopTaunt ) + { + bStopTaunt |= ShouldStopTaunting(); + } + + if ( bStopTaunt ) + { + CancelTaunt(); + } + } + + if ( ( RocketJumped() || StickyJumped() ) && IsAlive() && m_bCreatedRocketJumpParticles == false ) + { + const char *pEffectName = "rocketjump_smoke"; + DispatchParticleEffect( pEffectName, PATTACH_POINT_FOLLOW, this, "foot_L" ); + DispatchParticleEffect( pEffectName, PATTACH_POINT_FOLLOW, this, "foot_R" ); + m_bCreatedRocketJumpParticles = true; + } + + if ( !m_bCollideWithSentry ) + { + if ( IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + CBaseObject *pSentry = GetObjectOfType( OBJ_SENTRYGUN ); + if ( !pSentry ) + { + m_bCollideWithSentry = true; + } + else + { + if ( ( pSentry->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr() > 2500 ) + { + m_bCollideWithSentry = true; + } + } + } + else + { + m_bCollideWithSentry = true; + } + } + + if ( gpGlobals->curtime > m_flCommentOnCarrying && (m_flCommentOnCarrying != 0.f) ) + { + m_flCommentOnCarrying = 0.f; + + CBaseObject* pObj = m_Shared.GetCarriedObject(); + if ( pObj ) + { + SpeakConceptIfAllowed( MP_CONCEPT_CARRYING_BUILDING, pObj->GetResponseRulesModifier() ); + } + } + +#ifdef TF_RAID_MODE + CTFNavArea *area = (CTFNavArea *)GetLastKnownArea(); + if ( area && area->HasAttributeTF( TF_NAV_RESCUE_CLOSET ) ) + { + // we're standing in a rescue closet and need a friend to let us out - call for help! + SpeakConceptIfAllowed( MP_CONCEPT_PLAYER_HELP ); + } +#endif + + // Wrenchmotron taunt effect + if ( m_bIsTeleportingUsingEurekaEffect ) + { + if ( m_teleportHomeFlashTimer.HasStarted() && m_teleportHomeFlashTimer.IsElapsed() ) + { + m_teleportHomeFlashTimer.Invalidate(); + + if ( !tf_test_teleport_home_fx.GetBool() ) + { + // cover up the end of the taunt with a flash + color32 colorHit = { 255, 255, 255, 255 }; + UTIL_ScreenFade( this, colorHit, 0.25f, 0.25f, FFADE_IN ); + } + + Vector origin = GetAbsOrigin(); + CPVSFilter filter( origin ); + + UserMessageBegin( filter, "PlayerTeleportHomeEffect" ); + WRITE_BYTE( entindex() ); + MessageEnd(); + + // DispatchParticleEffect( "drg_wrenchmotron_teleport", PATTACH_ABSORIGIN ); + + switch( GetTeamNumber() ) + { + case TF_TEAM_RED: + TE_TFParticleEffect( filter, 0.0, "teleported_red", origin, vec3_angle ); + TE_TFParticleEffect( filter, 0.0, "player_sparkles_red", origin, vec3_angle, this, PATTACH_POINT ); + break; + case TF_TEAM_BLUE: + TE_TFParticleEffect( filter, 0.0, "teleported_blue", origin, vec3_angle ); + TE_TFParticleEffect( filter, 0.0, "player_sparkles_blue", origin, vec3_angle, this, PATTACH_POINT ); + break; + default: + break; + } + } + + // teleport home when taunt finishes + if ( !IsTaunting() ) + { + // drop the intel and any powerup we are carrying + DropFlag(); + DropRune(); + + EmitSound( "Building_Teleporter.Send" ); + m_bIsTeleportingUsingEurekaEffect = false; + + CObjectTeleporter* pTeleExit = assert_cast< CObjectTeleporter* >( GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT ) ); + + // Check if they wanted to go to their teleporter AND their teleporter can accept them + if ( m_eEurekaTeleportTarget == EUREKA_TELEPORT_TELEPORTER_EXIT && pTeleExit && ( pTeleExit->GetState() != TELEPORTER_STATE_BUILDING ) ) + { + pTeleExit->RecieveTeleportingPlayer( this ); + } + else + { + // Default to the spawn + TFGameRules()->GetPlayerSpawnSpot( this ); + } + } + } + + // Send active weapon's clip state to attached medics + bool bSendClipInfo = gpGlobals->curtime > m_flNextClipSendTime && + m_Shared.GetNumHealers() && + IsAlive(); + if ( bSendClipInfo ) + { + CTFWeaponBase *pTFWeapon = GetActiveTFWeapon(); + if ( pTFWeapon ) + { + int nClip = 0; + + if ( m_Shared.InCond( TF_COND_DISGUISED ) ) + { + nClip = m_Shared.GetDisguiseAmmoCount(); + } + else + { + nClip = pTFWeapon->UsesClipsForAmmo1() ? pTFWeapon->Clip1() : GetAmmoCount( pTFWeapon->GetPrimaryAmmoType() ); + } + + if ( nClip >= 0 && nClip != m_nActiveWpnClipPrev ) + { + if ( nClip > 500 ) + { + Warning( "Heal Target: ClipSize Data Limit Exceeded: %d (max 500)\n", nClip ); + nClip = MIN( nClip, 500 ); + } + m_nActiveWpnClip.Set( nClip ); + m_nActiveWpnClipPrev = m_nActiveWpnClip; + m_flNextClipSendTime = gpGlobals->curtime + 0.25f; + } + } + } + + if ( GetPlayerClass()->GetClassIndex() == TF_CLASS_SPY && ( GetFlags() & FL_DUCKING ) && ( pGroundEntity != NULL ) ) + { + int nDisguiseAsDispenserOnCrouch = 0; + CALL_ATTRIB_HOOK_FLOAT( nDisguiseAsDispenserOnCrouch, disguise_as_dispenser_on_crouch ); + if ( nDisguiseAsDispenserOnCrouch != 0 ) + { + m_Shared.AddCond( TF_COND_DISGUISED_AS_DISPENSER, 0.5f ); + } + } + + // rune charge over time + if ( m_Shared.CanRuneCharge() && !m_Shared.IsRuneCharged() ) + { + float dt = gpGlobals->curtime - m_flLastRuneChargeUpdate; + float flAdd = dt * 100.f / tf_powerup_max_charge_time.GetFloat(); + m_Shared.SetRuneCharge( m_Shared.GetRuneCharge() + flAdd ); + + if (m_Shared.GetCarryingRuneType() == RUNE_SUPERNOVA && m_Shared.IsRuneCharged() ) + { + ClientPrint( this, HUD_PRINTCENTER, "#TF_Powerup_Supernova_Deploy" ); + } + } + m_flLastRuneChargeUpdate = gpGlobals->curtime; + + // You can't touch a hooked target, so transmit plague when you get as close as you can + if ( GetGrapplingHookTarget() && GetGrapplingHookTarget()->IsPlayer() && m_Shared.GetCarryingRuneType() == RUNE_PLAGUE ) + { + CTFPlayer *pHookedPlayer = ToTFPlayer( GetGrapplingHookTarget() ); + + float flDistSqrToTarget = GetAbsOrigin().DistToSqr( pHookedPlayer->GetAbsOrigin() ); + if ( flDistSqrToTarget < 8100 && !pHookedPlayer->m_Shared.InCond( TF_COND_PLAGUE ) && + !m_Shared.IsAlly( pHookedPlayer ) && + !pHookedPlayer->m_Shared.IsInvulnerable() && + pHookedPlayer->m_Shared.GetCarryingRuneType() != RUNE_RESIST ) + { + pHookedPlayer->m_Shared.AddCond( TF_COND_PLAGUE, PERMANENT_CONDITION, this ); + } + } + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + // prevents player from standing on bot's head to block its movement. + if ( pGroundEntity && pGroundEntity->IsPlayer() ) + { + Vector vPush = GetAbsOrigin() - pGroundEntity->GetAbsOrigin(); + vPush.z = 0.f; + vPush.NormalizeInPlace(); + vPush.z = 1.f; + vPush *= 100.f; + + ApplyAbsVelocityImpulse( vPush ); + } + } + + // Scale our head + m_flHeadScale = Approach( GetDesiredHeadScale(), m_flHeadScale, GetHeadScaleSpeed() ); + + // scale our torso + m_flTorsoScale = Approach( GetDesiredTorsoScale(), m_flTorsoScale, GetTorsoScaleSpeed() ); + + // scale our torso + m_flHandScale = Approach( GetDesiredHandScale(), m_flHandScale, GetHandScaleSpeed() ); + +/* +#ifdef STAGING_ONLY + if ( m_Shared.InCond( TF_COND_SPACE_GRAVITY ) ) + { + // JetPack testing + if ( m_nButtons & IN_JUMP && !( GetFlags() & FL_ONGROUND ) && m_Shared.GetSpaceJumpChargeMeter() > tf_space_thrust_use_rate.GetFloat() ) + { + //mv->m_vecVelocity[2] += 10.0f; + Vector vThrust = Vector(0,0,0); + switch( GetPlayerClass()->GetClassIndex() ) + { + case TF_CLASS_SCOUT : vThrust.z = tf_space_thrust_scout.GetFloat(); break; + case TF_CLASS_SNIPER : vThrust.z = tf_space_thrust_sniper.GetFloat(); break; + case TF_CLASS_SOLDIER : vThrust.z = tf_space_thrust_soldier.GetFloat(); break; + case TF_CLASS_DEMOMAN : vThrust.z = tf_space_thrust_demo.GetFloat(); break; + case TF_CLASS_MEDIC : vThrust.z = tf_space_thrust_medic.GetFloat(); break; + case TF_CLASS_HEAVYWEAPONS : vThrust.z = tf_space_thrust_heavy.GetFloat(); break; + case TF_CLASS_PYRO : vThrust.z = tf_space_thrust_pyro.GetFloat(); break; + case TF_CLASS_SPY : vThrust.z = tf_space_thrust_spy.GetFloat(); break; + case TF_CLASS_ENGINEER : vThrust.z = tf_space_thrust_engy.GetFloat(); break; + } + + ApplyAbsVelocityImpulse( vThrust ); + + m_Shared.SetSpaceJumpChargeMeter( m_Shared.GetSpaceJumpChargeMeter() - tf_space_thrust_use_rate.GetFloat() ); + } + else + { + if ( GetFlags() & FL_ONGROUND ) + { + m_Shared.SetSpaceJumpChargeMeter( m_Shared.GetSpaceJumpChargeMeter() + tf_space_thrust_recharge_rate.GetFloat() ); + } + } + } +#endif +*/ + + SetContextThink( &CTFPlayer::TFPlayerThink, gpGlobals->curtime, "TFPlayerThink" ); + m_flLastThinkTime = gpGlobals->curtime; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns a portion of health every think. +//----------------------------------------------------------------------------- +void CTFPlayer::RegenThink( void ) +{ + if ( !IsAlive() ) + return; + + // Queue the next think + SetContextThink( &CTFPlayer::RegenThink, gpGlobals->curtime + TF_REGEN_TIME, "RegenThink" ); + + // if we're going in to this too often, quit out. + if ( m_flLastHealthRegenAt + TF_REGEN_TIME > gpGlobals->curtime ) + return; + + bool bShowRegen = true; + + // Medic has a base regen amount + if ( GetPlayerClass()->GetClassIndex() == TF_CLASS_MEDIC ) + { + // Heal faster if we haven't been in combat for a while. + float flTimeSinceDamage = gpGlobals->curtime - GetLastDamageReceivedTime(); + float flScale = RemapValClamped( flTimeSinceDamage, 5.0f, 10.0f, 1.0f, 2.0f ); + float flRegenAmt = TF_REGEN_AMOUNT; + + // If you are healing a hurt patient, increase your base regen + CTFPlayer *pPatient = ToTFPlayer( MedicGetHealTarget() ); + if ( pPatient && pPatient->GetHealth() < pPatient->GetMaxHealth() ) + { + // Double regen amount + flRegenAmt += TF_REGEN_AMOUNT; + } + + flRegenAmt *= flScale; + + // If the medic has this attribute, increase their regen. + if ( TFGameRules() && TFGameRules()->GameModeUsesUpgrades() ) + { + int iHealingMastery = 0; + CALL_ATTRIB_HOOK_INT( iHealingMastery, healing_mastery ); + if ( iHealingMastery ) + { + float flPerc = RemapValClamped( (float)iHealingMastery, 1.f, 4.f, 1.25f, 2.f ); + flRegenAmt *= flPerc; + } + } + + m_flAccumulatedHealthRegen += flRegenAmt; + + bShowRegen = false; + } + + // Other classes can be regenerated by items + float flRegenAmount = 0; + CALL_ATTRIB_HOOK_FLOAT( flRegenAmount, add_health_regen ); + if ( flRegenAmount ) + { + float flTimeSinceDamage = gpGlobals->curtime - GetLastDamageReceivedTime(); + float flScale = 1.0f; + // Ignore Scale for MvM, always give full regen + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + flScale = 1.0f; + } + else if ( flTimeSinceDamage < 5.0f ) + { + flScale = 0.25f; + } + else + { + flScale = RemapValClamped( flTimeSinceDamage, 5.0f, 10.0f, 0.5f, 1.0f ); + } + + flRegenAmount *= flScale; + } + m_flAccumulatedHealthRegen += flRegenAmount; + +// if ( m_Shared.InCond( TF_COND_HEALING_DEBUFF ) ) +// { +// m_flAccumulatedHealthRegen *= 0.75f; +// } + + int nHealAmount = 0; + if ( m_flAccumulatedHealthRegen >= 1.f ) + { + nHealAmount = floor( m_flAccumulatedHealthRegen ); + if ( GetHealth() < GetMaxHealth() ) + { + int nHealedAmount = TakeHealth( nHealAmount, DMG_GENERIC | DMG_IGNORE_DEBUFFS ); + if ( nHealedAmount > 0 ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "player_healed" ); + if ( event ) + { + event->SetInt( "priority", 1 ); // HLTV event priority + event->SetInt( "patient", GetUserID() ); + event->SetInt( "healer", GetUserID() ); + event->SetInt( "amount", nHealedAmount ); + gameeventmanager->FireEvent( event ); + } + } + } + } + else if ( m_flAccumulatedHealthRegen < -1.f ) + { + nHealAmount = ceil( m_flAccumulatedHealthRegen ); + TakeDamage( CTakeDamageInfo( this, this, NULL, vec3_origin, WorldSpaceCenter(), nHealAmount * -1, DMG_GENERIC ) ); + } + + if ( GetHealth() < GetMaxHealth() && nHealAmount != 0 && bShowRegen ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "player_healonhit" ); + if ( event ) + { + event->SetInt( "amount", nHealAmount ); + event->SetInt( "entindex", entindex() ); + event->SetInt( "weapon_def_index", INVALID_ITEM_DEF_INDEX ); + gameeventmanager->FireEvent( event ); + } + } + + m_flAccumulatedHealthRegen -= nHealAmount; + m_flLastHealthRegenAt = gpGlobals->curtime; + + // Regenerate ammo + if ( m_flNextAmmoRegenAt < gpGlobals->curtime ) + { + // We regen ammo every 5 seconds + m_flNextAmmoRegenAt = gpGlobals->curtime + 5.0; + + flRegenAmount = 0; + CALL_ATTRIB_HOOK_FLOAT( flRegenAmount, addperc_ammo_regen ); + + if ( flRegenAmount ) + { + RegenAmmoInternal( TF_AMMO_PRIMARY, flRegenAmount ); + RegenAmmoInternal( TF_AMMO_SECONDARY, flRegenAmount ); + } + + // Regenerate metal + int iMetal = 0; + CALL_ATTRIB_HOOK_INT( iMetal, add_metal_regen ); + + if ( iMetal ) + { + GiveAmmo( iMetal, TF_AMMO_METAL, true ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Returns a portion of health every think. +//----------------------------------------------------------------------------- +void CTFPlayer::RuneRegenThink( void ) +{ + if ( !IsAlive() ) + return; + + // Queue the next think + SetContextThink( &CTFPlayer::RuneRegenThink, gpGlobals->curtime + TF_REGEN_TIME_RUNE, "RuneRegenThink" ); + + // if we're going in to this too often, quit out. + if ( m_flLastRuneHealthRegenAt + TF_REGEN_TIME_RUNE > gpGlobals->curtime ) + return; + + int nRuneType = m_Shared.GetCarryingRuneType(); + if ( nRuneType == RUNE_NONE && !HasTheFlag() ) + return; + + // Regenerate health + float flAmount = 0.f; + switch ( GetPlayerClass()->GetClassIndex() ) + { + case TF_CLASS_SCOUT: + case TF_CLASS_SPY: + flAmount = 16; + break; + case TF_CLASS_SNIPER: + case TF_CLASS_ENGINEER: + flAmount = 14; + break; + case TF_CLASS_MEDIC: + case TF_CLASS_DEMOMAN: + case TF_CLASS_PYRO: + flAmount = 12; + break; + case TF_CLASS_SOLDIER: + flAmount = 10; + break; + case TF_CLASS_HEAVYWEAPONS: + flAmount = 8; + break; + } + if ( nRuneType == RUNE_REGEN ) + { + m_flAccumulatedRuneHealthRegen += flAmount; + } + // King and buffed team mates get some health regeneration unless they have the plague + else if ( ( nRuneType == RUNE_KING || m_Shared.InCond( TF_COND_KING_BUFFED ) ) && !m_Shared.InCond( TF_COND_PLAGUE ) ) + { + flAmount *= 0.3; + m_flAccumulatedRuneHealthRegen += flAmount; + } + // non powered up flag carriers get a small health regeneration + else if ( HasTheFlag() && nRuneType == RUNE_NONE ) + { + flAmount *= 0.1; + m_flAccumulatedRuneHealthRegen += flAmount; + } + + int nHealAmount = 0; + if ( m_flAccumulatedRuneHealthRegen >= 1.0 ) + { + nHealAmount = floor( m_flAccumulatedRuneHealthRegen ); + if ( GetHealth() < GetMaxHealth() ) + { + TakeHealth( nHealAmount, DMG_GENERIC ); + int nHealedAmount = TakeHealth( nHealAmount, DMG_GENERIC ); + if ( nHealedAmount > 0 ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "player_healed" ); + if ( event ) + { + event->SetInt("priority", 1); // HLTV event priority + event->SetInt("patient", GetUserID()); + event->SetInt( "healer", GetUserID() ); + event->SetInt( "amount", nHealedAmount ); + gameeventmanager->FireEvent( event ); + } + } + } + } + else if ( m_flAccumulatedRuneHealthRegen < -1.0 ) + { + nHealAmount = ceil( m_flAccumulatedRuneHealthRegen ); + TakeDamage( CTakeDamageInfo( this, this, NULL, vec3_origin, WorldSpaceCenter(), nHealAmount * -1, DMG_GENERIC ) ); + } + + if ( GetHealth() < GetMaxHealth() && nHealAmount != 0 ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "player_healonhit" ); + if ( event ) + { + event->SetInt( "amount", nHealAmount ); + event->SetInt( "entindex", entindex() ); + event->SetInt( "weapon_def_index", INVALID_ITEM_DEF_INDEX ); + gameeventmanager->FireEvent( event ); + } + } + + m_flAccumulatedRuneHealthRegen -= nHealAmount; + m_flLastRuneHealthRegenAt = gpGlobals->curtime; + + // Regenerate ammo and metal + if ( m_flNextRuneAmmoRegenAt < gpGlobals->curtime ) + { + m_flNextRuneAmmoRegenAt = gpGlobals->curtime + 5; + + if ( nRuneType == RUNE_REGEN ) + { + RegenAmmoInternal( TF_AMMO_PRIMARY, 0.5f ); + RegenAmmoInternal( TF_AMMO_SECONDARY, 0.5f ); + GiveAmmo( 200, TF_AMMO_METAL, true ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::RegenAmmoInternal( int iIndex, float flRegen ) +{ + m_flAccumulatedAmmoRegens[iIndex] += flRegen; + + // As soon as we have enough accumulated to regen a single unit of ammo, do it. + int iMaxAmmo = GetMaxAmmo(iIndex); + int iAmmo = m_flAccumulatedAmmoRegens[iIndex] * iMaxAmmo; + if ( iAmmo >= 1 ) + { + GiveAmmo( iAmmo, iIndex, true ); + m_flAccumulatedAmmoRegens[iIndex] -= ((float)iAmmo / (float)iMaxAmmo); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTFPlayer::~CTFPlayer() +{ + delete [] m_damageRateArray; + + DestroyRagdoll(); + m_PlayerAnimState->Release(); + + FOR_EACH_VEC( m_ItemsToTest, i ) + { + int iDef = TESTITEM_DEFINITIONS_BEGIN_AT + m_ItemsToTest[i].scriptItem.GetItemDefIndex(); + ItemSystem()->GetItemSchema()->ItemTesting_DiscardTestDefinition( iDef ); + + m_ItemsToTest[i].pKV->deleteThis(); + m_ItemsToTest[i].pKV = NULL; + } + + if ( m_hReviveMarker ) + { + UTIL_Remove( m_hReviveMarker ); + m_hReviveMarker = NULL; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +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(); +} + +//----------------------------------------------------------------------------- +// Estimate where a projectile fired from the given weapon will initially hit (it may bounce on from there). +// NOTE: We should be able to directly compute this knowing initial velocity, angle, gravity, etc, +// but I have been unable to find a formula that reproduces what our physics actually +// do. +//----------------------------------------------------------------------------- +Vector CTFPlayer::EstimateProjectileImpactPosition( CTFWeaponBaseGun *weapon ) +{ + if ( !weapon ) + { + return GetAbsOrigin(); + } + + const QAngle &angles = EyeAngles(); + + float initVel = weapon->IsWeapon( TF_WEAPON_PIPEBOMBLAUNCHER ) ? 900.0f : weapon->GetProjectileSpeed(); + CALL_ATTRIB_HOOK_FLOAT( initVel, mult_projectile_range ); + + return EstimateProjectileImpactPosition( angles.x, angles.y, initVel ); +} + +//----------------------------------------------------------------------------- +// Estimate where a stickybomb projectile will hit, +// using given pitch, yaw, and weapon charge (0-1) +//----------------------------------------------------------------------------- +Vector CTFPlayer::EstimateStickybombProjectileImpactPosition( float pitch, float yaw, float charge ) +{ + // estimate impact spot + float initVel = charge * ( TF_PIPEBOMB_MAX_CHARGE_VEL - TF_PIPEBOMB_MIN_CHARGE_VEL ) + TF_PIPEBOMB_MIN_CHARGE_VEL; + CALL_ATTRIB_HOOK_FLOAT( initVel, mult_projectile_range ); + + return EstimateProjectileImpactPosition( pitch, yaw, initVel ); +} + +//----------------------------------------------------------------------------- +// Estimate where a projectile fired will initially hit (it may bounce on from there), +// using given pitch, yaw, and initial velocity. +//----------------------------------------------------------------------------- +Vector CTFPlayer::EstimateProjectileImpactPosition( float pitch, float yaw, float initVel ) +{ + // copied from CTFWeaponBaseGun::FirePipeBomb() + Vector vecForward, vecRight, vecUp; + QAngle angles( pitch, yaw, 0.0f ); + AngleVectors( angles, &vecForward, &vecRight, &vecUp ); + + // we will assume bots never flip viewmodels + float fRight = 8.f; + Vector vecSrc = Weapon_ShootPosition(); + vecSrc += vecForward * 16.0f + vecRight * fRight + vecUp * -6.0f; + + const float initVelScale = 0.9f; + Vector vecVelocity = initVelScale * ( ( vecForward * initVel ) + ( vecUp * 200.0f ) ); + + const float timeStep = 0.01f; + const float maxTime = 5.0f; + + Vector pos = vecSrc; + Vector lastPos = pos; + const float g = GetCurrentGravity(); + + + // compute forward facing unit vector in horiz plane + Vector alongDir = vecForward; + alongDir.z = 0.0f; + alongDir.NormalizeInPlace(); + + float alongVel = FastSqrt( vecVelocity.x * vecVelocity.x + vecVelocity.y * vecVelocity.y ); + + trace_t trace; + NextBotTraceFilterIgnoreActors traceFilter( NULL, COLLISION_GROUP_NONE ); + + float t; + for( t = 0.0f; t < maxTime; t += timeStep ) + { + float along = alongVel * t; + float height = vecVelocity.z * t - 0.5f * g * t * t; + + pos.x = vecSrc.x + alongDir.x * along; + pos.y = vecSrc.y + alongDir.y * along; + pos.z = vecSrc.z + height; + + UTIL_TraceHull( lastPos, pos, -Vector(8,8,8), Vector(8,8,8), MASK_SOLID_BRUSHONLY, &traceFilter, &trace ); + + if ( trace.DidHit() ) + { +#ifdef STAGING_ONLY + if ( tf_debug_ballistics.GetBool() ) + { + NDebugOverlay::Cross3D( trace.endpos, 10.0f, 100, 255, 0, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } +#endif + break; + } + +#ifdef STAGING_ONLY + if ( tf_debug_ballistics.GetBool() ) + { + NDebugOverlay::Line( lastPos, pos, 0, 255, 0, false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } +#endif + + lastPos = pos; + } + + return trace.endpos; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::ProcessSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) +{ + // TF Players only process scene events on the server while running taunts + if ( !IsTaunting() ) + return false; + + // Only process sequences + if ( event->GetType() != CChoreoEvent::SEQUENCE ) + return false; + + return BaseClass::ProcessSceneEvent( info, scene, event ); +} + +//----------------------------------------------------------------------------- +// 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(); + + ProcessSceneEvents(); + + if ( TFGameRules()->IsInArenaMode() == true ) + { + if ( TFGameRules()->State_Get() != GR_STATE_TEAM_WIN ) + { + if ( GetTeamNumber() == TEAM_SPECTATOR ) + { + m_Local.m_iHideHUD &= ~HIDEHUD_MISCSTATUS; + } + } + } + + // Hype Decreases over time + if ( IsPlayerClass( TF_CLASS_SCOUT ) ) + { + float flHypeDecays = 0; + CALL_ATTRIB_HOOK_FLOAT( flHypeDecays, hype_decays_over_time ); + + if ( flHypeDecays != 0 ) + { + // Loose hype over time + float flHype = m_Shared.GetScoutHypeMeter(); + flHype = flHype - flHypeDecays; + m_Shared.SetScoutHypeMeter( flHype ); + TeamFortress_SetSpeed(); + } + } + +#ifdef STAGING_ONLY + // show ballistic path for currently equipped weapon (ie: grenades) + if ( tf_debug_ballistics.GetBool() ) + { + CTFWeaponBaseGun *myWeapon = dynamic_cast< CTFWeaponBaseGun * >( m_Shared.GetActiveTFWeapon() ); + EstimateProjectileImpactPosition( myWeapon ); + } + + if ( tf_debug_ballistic_targeting.GetBool() ) + { + CTFWeaponBaseGun *myWeapon = dynamic_cast< CTFWeaponBaseGun * >( m_Shared.GetActiveTFWeapon() ); + Vector hitPos = EstimateProjectileImpactPosition( myWeapon ); + + Vector toEye = EyePosition() - hitPos; + Vector toTarget = tf_debug_ballistic_target - hitPos; + float error = toTarget.NormalizeInPlace(); + + if ( error < 20.0f ) + { + // on target + NDebugOverlay::Cross3D( tf_debug_ballistic_target, 10.0f, 0, 255, 0, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + else + { + if ( DotProduct( toEye, toTarget ) > 0.0f ) + { + // too far + NDebugOverlay::Cross3D( tf_debug_ballistic_target, 10.0f, 0, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + NDebugOverlay::VertArrow( tf_debug_ballistic_target, tf_debug_ballistic_target - Vector( 0, 0, error ), 10.0f, 0, 0, 255, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + else + { + // too near + NDebugOverlay::Cross3D( tf_debug_ballistic_target, 10.0f, 255, 0, 0, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + NDebugOverlay::VertArrow( tf_debug_ballistic_target, tf_debug_ballistic_target + Vector( 0, 0, error ), 10.0f, 255, 0, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } +#endif // STAGING_ONLY +} + +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)" ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::CheckForIdle( void ) +{ + if ( m_afButtonLast != m_nButtons ) + m_flLastAction = gpGlobals->curtime; + + if ( mp_idledealmethod.GetInt() ) + { + if ( IsHLTV() || IsReplay() ) + return; + + if ( IsFakeClient() ) + return; + + if ( IsCoaching() && GetStudent() != NULL ) + return; + + if ( TFGameRules() && TFGameRules()->ShowMatchSummary() ) + return; + + if ( TFGameRules()->State_Get() == GR_STATE_BETWEEN_RNDS ) + 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 ( IsAutoKickDisabled() ) + return; + + const bool cbMoving = ( m_nButtons & ( IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT ) ) != 0; + + m_bIsAFK = false; + + if ( !cbMoving && PointInRespawnRoom( this, WorldSpaceCenter() ) ) + { + m_flTimeInSpawn += TICK_INTERVAL; + } + else + m_flTimeInSpawn = 0; + + if ( TFGameRules()->IsInArenaMode() && tf_arena_use_queue.GetBool() == true ) + { + if ( GetTeamNumber() == TEAM_SPECTATOR ) + return; + + if ( TFGameRules()->State_Get() == GR_STATE_TEAM_WIN && TFGameRules()->GetWinningTeam() != GetTeamNumber() ) + { + if ( m_bArenaIsAFK ) + { + m_bIsAFK = true; + m_bArenaIsAFK = false; + } + } + } + else + { + // Cannot possibly get out of the spawn room in 0 seconds--so if the ConVar says 0, let's assume 30 seconds. + float flIdleTime = Max( mp_idlemaxtime.GetFloat() * 60, 30.0f ); + + if ( TFGameRules()->InStalemate() ) + { + flIdleTime = mp_stalemate_timelimit.GetInt() * 0.5f; + } + + m_bIsAFK = ( gpGlobals->curtime - m_flLastAction ) > flIdleTime + || ( m_flTimeInSpawn > flIdleTime ); + } + + if ( m_bIsAFK == true ) + { + bool bKickPlayer = false; + + ConVarRef mp_allowspectators( "mp_allowspectators" ); + if ( ( mp_allowspectators.IsValid() && mp_allowspectators.GetBool() == false ) || ( TFGameRules()->IsInArenaMode() && tf_arena_use_queue.GetBool() ) ) + { + // just kick the player if this server doesn't allow spectators + bKickPlayer = true; + } + else if ( mp_idledealmethod.GetInt() == 1 ) + { + if ( GetTeamNumber() < FIRST_GAME_TEAM ) + { + bKickPlayer = true; + } + else + { + //First send them into spectator mode then kick him. + ForceChangeTeam( TEAM_SPECTATOR ); + m_flLastAction = gpGlobals->curtime; + m_flTimeInSpawn = 0; + return; + } + } + 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 %s\n", GetUserID(), g_pszIdleKickString ) ); + m_flLastAction = gpGlobals->curtime; + m_flTimeInSpawn = 0; + } + } + } +} + +extern ConVar flashlight; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTFPlayer::FlashlightIsOn( void ) +{ + return IsEffectActive( EF_DIMLIGHT ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::FlashlightTurnOn( void ) +{ + if( flashlight.GetInt() > 0 && IsAlive() ) + { + AddEffects( EF_DIMLIGHT ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::FlashlightTurnOff( void ) +{ + if( IsEffectActive(EF_DIMLIGHT) ) + { + RemoveEffects( EF_DIMLIGHT ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Update Halloween scenario effects on players. +//----------------------------------------------------------------------------- +void CTFPlayer::UpdateHalloween( void ) +{ + // This is a push force + if ( !m_vHalloweenKartPush.IsZero() ) + { + if ( !m_Shared.InCond( TF_COND_INVULNERABLE_USER_BUFF ) ) + { + CPVSFilter filter( GetAbsOrigin() ); + TE_TFParticleEffect( filter, 0.0, "kart_impact_sparks", GetAbsOrigin(), vec3_angle, this, PATTACH_ABSORIGIN ); + + float flStunDuration = m_vHalloweenKartPush.Length() / 1000.0f; + if ( m_vHalloweenKartPush.LengthSqr() > 1000 * 1000 ) + { + TE_TFParticleEffect( filter, 0.0, "kartimpacttrail", GetAbsOrigin(), vec3_angle, this, PATTACH_ABSORIGIN_FOLLOW ); + EmitSound( "BumperCar.BumpIntoAir" ); + EmitSound( "BumperCar.BumpHard" ); + } + else + { + EmitSound( "BumperCar.Bump" ); + } + + if ( tf_halloween_kart_stun_enabled.GetBool() ) + { + m_Shared.StunPlayer( flStunDuration * tf_halloween_kart_stun_duration_scale.GetFloat(), tf_halloween_kart_stun_amount.GetFloat(), TF_STUN_BOTH | TF_STUN_NO_EFFECTS ); + } + + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART_DASH ) ) + { + m_Shared.RemoveCond( TF_COND_HALLOWEEN_KART_DASH ); + } + + ApplyAirBlastImpulse( m_vHalloweenKartPush ); + } + + m_vHalloweenKartPush.Zero(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::AddHalloweenKartPushEvent( CTFPlayer *pOther, CBaseEntity *pInflictor, CBaseEntity *pWeapon, Vector vForce, int iDamage, int iDamageType /* = 0 */ ) +{ + // Create a damage event so they can get credit for the kill + //m_vHalloweenKartPushEventTime + 0.2 > gpGlobals->curtime && + if ( m_Shared.InCond( TF_COND_INVULNERABLE_USER_BUFF ) ) + return; + + // Ignore small forces + float flForce = vForce.LengthSqr(); + if ( flForce < 100.0f ) + return; + + float flExtraMultiplier = 1.f; + if ( pOther && pOther != this && pOther->m_Shared.InCond( TF_COND_HALLOWEEN_BOMB_HEAD ) ) + { + iDamage *= tf_halloween_kart_bomb_head_damage_scale.GetFloat(); + flExtraMultiplier = tf_halloween_kart_bomb_head_impulse_scale.GetFloat(); + pOther->SetKartBombHeadTarget( pOther ); + pOther->m_Shared.RemoveCond( TF_COND_HALLOWEEN_BOMB_HEAD ); + } + + const float flCurrentKartKnockbackMultiplier = GetKartKnockbackMultiplier( flExtraMultiplier ); + + if ( pOther ) + { + // Fake Damage + IGameEvent * event = gameeventmanager->CreateEvent( "player_hurt" ); + if ( event ) + { + int iKartDamageType = DMG_CLUB | DMG_PREVENT_PHYSICS_FORCE; + if ( pOther != this && flCurrentKartKnockbackMultiplier * vForce.LengthSqr() > 1000 * 1000 ) + { + iKartDamageType |= DMG_CRITICAL; + } + + event->SetInt( "userid", GetUserID() ); + event->SetInt( "health", MAX( 0, m_iHealth ) ); + + // HLTV event priority, not transmitted + event->SetInt( "priority", 5 ); + event->SetInt( "damageamount", iDamage ); + + // Hurt by another player. + event->SetInt( "attacker", pOther->GetUserID() ); + event->SetInt( "custom", TF_DMG_CUSTOM_SUICIDE ); + event->SetBool( "crit", ( iKartDamageType & DMG_CRITICAL ) != 0 ); + event->SetBool( "allseecrit", ( iKartDamageType & DMG_CRITICAL ) != 0 ); + event->SetInt( "bonuseffect", (int)kBonusEffect_None ); + // + gameeventmanager->FireEvent( event ); + } + + m_AchievementData.AddDamagerToHistory( pOther ); + m_AchievementData.AddPusherToHistory( pOther ); + //m_Shared.SetAssist( pOther ); + } + + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + m_iKartHealth += iDamage; + } + + // HHH + if ( TFGameRules()->IsIT( pOther ) ) + { + // Tag! You're IT! + TFGameRules()->SetIT( this ); + } + + if ( iDamageType == TF_DMG_CUSTOM_DECAPITATION_BOSS ) + { + m_flHHHKartAttackTime = gpGlobals->curtime; + } + + //m_vHalloweenKartPushEventTime = gpGlobals->curtime; + float flImpulseScale = 1.0f; + + // m_flKartHealth might change by this point, calculate new knock back multiplier + const float flNewKartKnockbackMultiplier = GetKartKnockbackMultiplier( flExtraMultiplier ); + + // push other + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + // If applying damage increase the force, otherwise its likely just a wall collision + if ( iDamage > 0 ) + { + vForce *= flImpulseScale * flNewKartKnockbackMultiplier; + vForce.z *= ( ( flNewKartKnockbackMultiplier - 1.0f ) * 0.20f ) + 1.0f; + + // Decrease all forces if in the air + if (!(GetFlags() & FL_ONGROUND) ) + { + vForce *= tf_halloween_kart_impact_air_scale.GetFloat(); + } + } + } + else + { + // Make non-karters take damage! + vForce *= ( flImpulseScale * 2.0f ); + + //ghostinfo.SetDamageCustom( TF_DMG_CUSTOM_KART ); + // Create a damage event based on Speed + float flDamage = vForce.Length() / 50.0f + RandomFloat( 3.0f, 7.0f ); + CTakeDamageInfo info; + info.SetAttacker( pOther ); + info.SetInflictor( pOther ); + info.SetDamage( flDamage ); + info.SetDamageCustom( TF_DMG_CUSTOM_KART ); + info.SetDamagePosition( GetAbsOrigin() ); + int iKartDamageType = DMG_CLUB; + if ( flDamage > 20 ) + { + iKartDamageType |= DMG_CRITICAL; + } + info.SetDamageType( iKartDamageType ); + info.SetDamageForce( vForce ); + TakeDamage( info ); + return; + } + + m_vHalloweenKartPush += vForce; + + // Dropped collection game tokens if hit hard enough and we're in the collection minigame + if ( pOther && iDamage > 10 && CTFMinigameLogic::GetMinigameLogic() && CTFMinigameLogic::GetMinigameLogic()->GetActiveMinigame() && + CTFMinigameLogic::GetMinigameLogic()->GetActiveMinigame()->GetMinigameType() == CTFMiniGame::EMinigameType::MINIGAME_HALLOWEEN2014_COLLECTION ) + { + CUtlVector< CTFPlayer* > vecEveryone; + CollectPlayers( &vecEveryone ); + int nNumPlayers = vecEveryone.Count(); + + // Drop tokens + uint32 nNumToSpawn = ( iDamage / 5 ) + 1; + nNumToSpawn = RemapValClamped( nNumPlayers, 8, 16, nNumToSpawn, 1 ); + + if ( gpGlobals->curtime > m_flNextBonusDucksVOAllowedTime ) + { + // Tell this user to play a "Bonus Ducks!" line. + CSingleUserRecipientFilter filter( pOther ); + UserMessageBegin( filter, "BonusDucks" ); + WRITE_BYTE( entindex() ); + WRITE_BYTE( false ); + MessageEnd(); + } + + while( nNumToSpawn-- ) + { + CHalloweenPickup *pPickup = dynamic_cast< CHalloweenPickup * >( CreateEntityByName( "tf_halloween_pickup" ) ); + if (pPickup) + { + pPickup->m_nSkin = 2; // Golden skin + pPickup->Precache(); + DispatchSpawn(pPickup); + Vector vecRandom = RandomVector( -200.f, 200.f ); + vecRandom.z = RandomFloat( 300.f, 400.f ); + Vector vecDropVector = vecRandom + vForce * 0.2f; + pPickup->DropSingleInstance( vecDropVector, this, 1.f, 1.f ); + + pPickup->SetAbsOrigin( GetAbsOrigin() + Vector( 0.f, 0.f, 40.f ) ); + + pPickup->Activate(); + } + } + } + + //DevMsg( "Kart Impact %fx,%fy,%fz - %f Base. %f Multiplayer, %f TotalForce, %d Damage, %i Class \n", + // vForce.x, vForce.y, vForce.z, vForce.Length(), flNewKartKnockbackMultiplier, vForce.Length() * flNewKartKnockbackMultiplier, iDamage, GetPlayerClass()->GetClassIndex() ); + + vForce.z = 0; + vForce.NormalizeInPlace(); + DispatchParticleEffect( "kart_impact_sparks", GetAbsOrigin() + vForce * 24, GetAbsAngles() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTFPlayer::GetKartKnockbackMultiplier( float flExtraMultiplier /*= 1.f*/ ) const +{ + return flExtraMultiplier * ( 1.0f + (float)m_iKartHealth / tf_halloween_kart_damage_to_force.GetFloat() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ResetKartDamage() +{ + m_iKartHealth = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::CancelEurekaTeleport() +{ + m_bIsTeleportingUsingEurekaEffect = false; + m_teleportHomeFlashTimer.Invalidate(); +} + +//----------------------------------------------------------------------------- +// 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] ); + + if ( m_flTauntAttackTime && m_flTauntAttackTime < gpGlobals->curtime ) + { + m_flTauntAttackTime = 0; + DoTauntAttack(); + } + + // if we are coaching, then capture events for adding annotations + if ( m_bIsCoaching && m_hStudent ) + { + if ( ( m_afButtonPressed & ( IN_ATTACK | IN_ATTACK2 ) ) != 0 ) + { + if ( m_afButtonPressed & IN_ATTACK ) + { + HandleCoachCommand( this, kCoachCommand_Attack ); + } + else if ( m_afButtonPressed & IN_ATTACK2 ) + { + HandleCoachCommand( this, kCoachCommand_Defend ); + } + } + if ( m_hStudent->GetTeamNumber() != TEAM_SPECTATOR ) + { + // tether coach to student--if the coach gets too far, move them toward the student + Vector vecTarget = m_hStudent->GetAbsOrigin(); + Vector vecDelta = GetAbsOrigin() - vecTarget; + float flDistance = vecDelta.Length(); + const float kInchesToMeters = 0.0254f; + const float kMetersToInches = 1.0f / kInchesToMeters; + const float kMaxDistanceToStudent = 30; + int distance = RoundFloatToInt( flDistance * kInchesToMeters ); + if ( distance > kMaxDistanceToStudent ) + { + VectorNormalize( vecDelta ); + SetAbsOrigin( vecTarget + vecDelta * ( kMaxDistanceToStudent * kMetersToInches ) ); + } + } + } + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + // metal is free during setup time + if ( TFGameRules()->IsQuickBuildTime() ) + { + GiveAmmo( 1000, TF_AMMO_METAL, true ); + } + + // clamp maximum velocity to avoid sending mini-bosses into the stratosphere + if ( GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + Vector ahead = GetAbsVelocity(); + float speed = ahead.NormalizeInPlace(); + + const float velocityLimit = 1000.0f; + if ( speed > velocityLimit ) + { + speed = velocityLimit; + } + + SetAbsVelocity( speed * ahead ); + } + } + + UpdateHalloween(); + +#ifdef STAGING_ONLY + m_Shared.DoRocketPack(); +#endif // STAGING_ONLY +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::PrecacheMvM() +{ + for ( int i = TF_FIRST_NORMAL_CLASS; i < TF_LAST_NORMAL_CLASS; ++i ) + { + COMPILE_TIME_ASSERT( ARRAYSIZE( g_szBotModels ) == TF_LAST_NORMAL_CLASS ); + int iModelIndex = PrecacheModel( g_szBotModels[ i ] ); + PrecacheGibsForModel( iModelIndex ); + + COMPILE_TIME_ASSERT( ARRAYSIZE( g_szBotBossModels ) == TF_LAST_NORMAL_CLASS ); + iModelIndex = PrecacheModel( g_szBotBossModels[ i ] ); + PrecacheGibsForModel( iModelIndex ); + } + + int iModelIndex = PrecacheModel( g_szBotBossSentryBusterModel ); + PrecacheGibsForModel( iModelIndex ); + + PrecacheModel( "models/items/currencypack_small.mdl" ); + PrecacheModel( "models/items/currencypack_medium.mdl" ); + PrecacheModel( "models/items/currencypack_large.mdl" ); + + PrecacheModel( "models/bots/tw2/boss_bot/twcarrier_addon.mdl" ); + + PrecacheParticleSystem( "bot_impact_light" ); + PrecacheParticleSystem( "bot_impact_heavy" ); + PrecacheParticleSystem( "bot_death" ); + PrecacheParticleSystem( "bot_radio_waves" ); + + PrecacheScriptSound( "MVM.BotStep" ); + PrecacheScriptSound( "MVM.GiantHeavyStep" ); + PrecacheScriptSound( "MVM.GiantSoldierStep" ); + PrecacheScriptSound( "MVM.GiantDemomanStep" ); + PrecacheScriptSound( "MVM.GiantScoutStep" ); + PrecacheScriptSound( "MVM.GiantPyroStep" ); + PrecacheScriptSound( "MVM.GiantHeavyLoop" ); + PrecacheScriptSound( "MVM.GiantSoldierLoop" ); + PrecacheScriptSound( "MVM.GiantDemomanLoop" ); + PrecacheScriptSound( "MVM.GiantScoutLoop" ); + PrecacheScriptSound( "MVM.GiantPyroLoop" ); + PrecacheScriptSound( "MVM.GiantHeavyExplodes" ); + PrecacheScriptSound( "MVM.GiantCommonExplodes" ); + PrecacheScriptSound( "MVM.SentryBusterExplode" ); + PrecacheScriptSound( "MVM.SentryBusterLoop" ); + PrecacheScriptSound( "MVM.SentryBusterIntro" ); + PrecacheScriptSound( "MVM.SentryBusterStep" ); + PrecacheScriptSound( "MVM.SentryBusterSpin" ); + PrecacheScriptSound( "MVM.DeployBombSmall" ); + PrecacheScriptSound( "MVM.DeployBombGiant" ); + PrecacheScriptSound( "Weapon_Upgrade.ExplosiveHeadshot" ); + PrecacheScriptSound( "Spy.MVM_Chuckle" ); + PrecacheScriptSound( "MVM.Robot_Engineer_Spawn" ); + PrecacheScriptSound( "MVM.Robot_Teleporter_Deliver" ); + PrecacheScriptSound( "MVM.MoneyPickup" ); + + PrecacheMaterial( "effects/circle_nocull" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::PrecacheKart() +{ + PrecacheModel( "models/player/items/taunts/bumpercar/parts/bumpercar.mdl" ); + PrecacheModel( "models/props_halloween/bumpercar_cage.mdl" ); + + PrecacheScriptSound( "BumperCar.Spawn" ); + PrecacheScriptSound( "BumperCar.SpawnFromLava" ); + PrecacheScriptSound( "BumperCar.GoLoop" ); + PrecacheScriptSound( "BumperCar.Screech" ); + PrecacheScriptSound( "BumperCar.HitGhost" ); + PrecacheScriptSound( "BumperCar.Bump" ); + PrecacheScriptSound( "BumperCar.BumpHard" ); + PrecacheScriptSound( "BumperCar.BumpIntoAir" ); + PrecacheScriptSound( "BumperCar.SpeedBoostStart" ); + PrecacheScriptSound( "BumperCar.SpeedBoostStop" ); + PrecacheScriptSound( "BumperCar.Jump" ); + PrecacheScriptSound( "BumperCar.JumpLand" ); + PrecacheScriptSound( "sf14.Merasmus.DuckHunt.BonusDucks" ); + + PrecacheParticleSystem( "kartimpacttrail" ); + PrecacheParticleSystem( "kart_dust_trail_red" ); + PrecacheParticleSystem( "kart_dust_trail_blue" ); + + PrecacheParticleSystem( "kartdamage_4"); +} + +//----------------------------------------------------------------------------- +// 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 ); + } + + pszModel = GetPlayerClassData( i )->m_szHandModelName; + if ( pszModel && pszModel[0] ) + { + PrecacheModel( pszModel ); + } + +/* + if ( !IsX360() ) + { + // Precache the hardware facial morphed models as well. + const char *pszHWMModel = GetPlayerClassData( i )->m_szHWMModelName; + if ( pszHWMModel && pszHWMModel[0] ) + { + PrecacheModel( pszHWMModel ); + } + } +*/ + } + + // Always precache the silly gibs. + for ( i = 4; i < ARRAYSIZE( g_pszBDayGibs ); ++i ) + { + PrecacheModel( g_pszBDayGibs[i] ); + } + + if ( TFGameRules() && TFGameRules()->IsBirthday() ) + { + for ( i = 0; i < 4/*ARRAYSIZE(g_pszBDayGibs)*/; i++ ) + { + PrecacheModel( g_pszBDayGibs[i] ); + } + PrecacheModel( "models/effects/bday_hat.mdl" ); + } + if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) ) + { + PrecacheModel( "models/props_halloween/halloween_gift.mdl" ); + PrecacheModel( "models/props_halloween/ghost_no_hat.mdl" ); + PrecacheModel( "models/props_halloween/ghost_no_hat_red.mdl" ); + } + + // Precache player class sounds + for ( i = TF_FIRST_NORMAL_CLASS; i < TF_CLASS_COUNT_ALL; ++i ) + { + TFPlayerClassData_t *pData = GetPlayerClassData( i ); + + for ( int i = 0; i < ARRAYSIZE( pData->m_szDeathSound ); ++i ) + { + PrecacheScriptSound( pData->m_szDeathSound[ i ] ); + } + } + +#ifdef STAGING_ONLY + PrecacheModel( "models/weapons/c_models/c_bread/c_bread_plainloaf.mdl" ); +#endif + + COMPILE_TIME_ASSERT( TF_CALLING_CARD_MODEL_COUNT == ARRAYSIZE( g_pszDeathCallingCardModels ) ); + // Precache, Deliberatly skipping zero + for ( i = 1; i < TF_CALLING_CARD_MODEL_COUNT; i++ ) + { + PrecacheModel( g_pszDeathCallingCardModels[i] ); + } + +#ifdef STAGING_ONLY + COMPILE_TIME_ASSERT( ARRAYSIZE( g_szPlayerRobotModels ) == TF_LAST_NORMAL_CLASS ); + for ( int i = TF_FIRST_NORMAL_CLASS; i < TF_LAST_NORMAL_CLASS; ++i ) + { + PrecacheModel( g_szPlayerRobotModels[i] ); + //int iModelIndex = PrecacheModel( g_szPlayerRobotModels[i] ); + //PrecacheGibsForModel( iModelIndex ); + } +#endif // STAGING_ONLY + +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::PrecacheTFPlayer() +{ + VPROF_BUDGET( "CTFPlayer::PrecacheTFPlayer", VPROF_BUDGETGROUP_PLAYER ); + if( !m_bTFPlayerNeedsPrecache ) + return; + + m_bTFPlayerNeedsPrecache = false; + + // Precache the player models and gibs. + PrecachePlayerModels(); + + // Precache the player sounds. + PrecacheScriptSound( "Player.Spawn" ); + PrecacheScriptSound( "TFPlayer.Pain" ); + PrecacheScriptSound( "TFPlayer.CritHit" ); + PrecacheScriptSound( "TFPlayer.CritHitMini" ); + PrecacheScriptSound( "TFPlayer.DoubleDonk" ); + PrecacheScriptSound( "TFPlayer.CritPain" ); + PrecacheScriptSound( "TFPlayer.CritDeath" ); + PrecacheScriptSound( "TFPlayer.FreezeCam" ); + PrecacheScriptSound( "TFPlayer.Drown" ); + PrecacheScriptSound( "TFPlayer.AttackerPain" ); + PrecacheScriptSound( "TFPlayer.SaveMe" ); + PrecacheScriptSound( "TFPlayer.CritBoostOn" ); + PrecacheScriptSound( "TFPlayer.CritBoostOff" ); + PrecacheScriptSound( "TFPlayer.Decapitated" ); + PrecacheScriptSound( "TFPlayer.ReCharged" ); + PrecacheScriptSound( "Camera.SnapShot" ); + PrecacheScriptSound( "TFPlayer.Dissolve" ); + + PrecacheScriptSound( "Saxxy.TurnGold" ); + + PrecacheScriptSound( "Icicle.TurnToIce" ); + PrecacheScriptSound( "Icicle.HitWorld" ); + PrecacheScriptSound( "Icicle.Melt" ); + + PrecacheScriptSound( "DemoCharge.ChargeCritOn" ); + PrecacheScriptSound( "DemoCharge.ChargeCritOff" ); + PrecacheScriptSound( "DemoCharge.Charging" ); + + PrecacheScriptSound( "TFPlayer.StunImpactRange" ); + PrecacheScriptSound( "TFPlayer.StunImpact" ); + PrecacheScriptSound( "Halloween.PlayerScream" ); + PrecacheScriptSound( "Halloween.PlayerEscapedUnderworld" ); + + PrecacheScriptSound( "Game.YourTeamLost" ); + PrecacheScriptSound( "Game.YourTeamWon" ); + PrecacheScriptSound( "Game.SuddenDeath" ); + PrecacheScriptSound( "Game.Stalemate" ); + PrecacheScriptSound( "TV.Tune" ); + + //This will be moved out once we do the announcer pass. + PrecacheScriptSound( "Announcer.AM_FirstBloodRandom" ); + PrecacheScriptSound( "Announcer.AM_CapEnabledRandom" ); + PrecacheScriptSound( "Announcer.AM_RoundStartRandom" ); + PrecacheScriptSound( "Announcer.AM_FirstBloodFast" ); + PrecacheScriptSound( "Announcer.AM_FirstBloodFinally" ); + PrecacheScriptSound( "Announcer.AM_FlawlessVictoryRandom" ); + PrecacheScriptSound( "Announcer.AM_FlawlessDefeatRandom" ); + PrecacheScriptSound( "Announcer.AM_FlawlessVictory01" ); + PrecacheScriptSound( "Announcer.AM_TeamScrambleRandom" ); + PrecacheScriptSound( "Taunt.MedicHeroic" ); + PrecacheScriptSound( "Taunt.GuitarRiff" ); + + // Dmg absorb sound + PrecacheScriptSound( "Powerup.ReducedDamage" ); + + // Tourney UI + PrecacheScriptSound( "Tournament.PlayerReady" ); + + PrecacheScriptSound( "Medic.AutoCallerAnnounce" ); + +#ifdef STAGING_ONLY + // Killstreak sounds + PrecacheScriptSound( "Announcer.KillStreak_Level1" ); + PrecacheScriptSound( "Announcer.KillStreak_Level2" ); + PrecacheScriptSound( "Announcer.KillStreak_Level3" ); + PrecacheScriptSound( "Announcer.KillStreak_Level4" ); + PrecacheScriptSound( "Game.KillStreak" ); +#endif + + // Precache particle systems + PrecacheParticleSystem( "crit_text" ); + PrecacheParticleSystem( "miss_text" ); + PrecacheParticleSystem( "cig_smoke" ); + PrecacheParticleSystem( "speech_mediccall" ); + PrecacheParticleSystem( "speech_mediccall_auto" ); + PrecacheParticleSystem( "speech_taunt_all" ); + PrecacheParticleSystem( "speech_taunt_red" ); + PrecacheParticleSystem( "speech_taunt_blue" ); + 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( "burningplayer_rainbow" ); + PrecacheParticleSystem( "blood_spray_red_01" ); + PrecacheParticleSystem( "blood_spray_red_01_far" ); + PrecacheParticleSystem( "pyrovision_blood" ); + + PrecacheParticleSystem( "water_blood_impact_red_01" ); + PrecacheParticleSystem( "blood_impact_red_01" ); + PrecacheParticleSystem( "water_playerdive" ); + PrecacheParticleSystem( "water_playeremerge" ); + PrecacheParticleSystem( "healthgained_red" ); + PrecacheParticleSystem( "healthgained_blu" ); + PrecacheParticleSystem( "healthgained_red_large" ); + PrecacheParticleSystem( "healthgained_blu_large" ); + PrecacheParticleSystem( "healthgained_red_giant" ); + PrecacheParticleSystem( "healthgained_blu_giant" ); + PrecacheParticleSystem( "critgun_weaponmodel_red" ); + PrecacheParticleSystem( "critgun_weaponmodel_blu" ); + PrecacheParticleSystem( "overhealedplayer_red_pluses" ); + PrecacheParticleSystem( "overhealedplayer_blue_pluses" ); + PrecacheParticleSystem( "highfive_red" ); + PrecacheParticleSystem( "highfive_blue" ); + PrecacheParticleSystem( "god_rays" ); + PrecacheParticleSystem( "bl_killtaunt" ); + PrecacheParticleSystem( "birthday_player_circling" ); + PrecacheParticleSystem( "drg_fiery_death" ); + PrecacheParticleSystem( "drg_wrenchmotron_teleport" ); + PrecacheParticleSystem( "taunt_flip_land_red" ); + PrecacheParticleSystem( "taunt_flip_land_blue" ); + PrecacheParticleSystem( "tfc_sniper_mist" ); + PrecacheParticleSystem( "dxhr_sniper_rail_blue" ); + PrecacheParticleSystem( "dxhr_sniper_rail_red" ); + PrecacheParticleSystem( "tfc_sniper_distortion_trail" ); + + for ( int i=0; i<ARRAYSIZE( s_pszTauntRPSParticleNames ); ++i ) + { + PrecacheParticleSystem( s_pszTauntRPSParticleNames[i] ); + } + + PrecacheParticleSystem( "blood_decap" ); + + PrecacheParticleSystem( "xms_icicle_idle" ); + PrecacheParticleSystem( "xms_icicle_impact" ); + PrecacheParticleSystem( "xms_icicle_impact_dryice" ); + PrecacheParticleSystem( "xms_icicle_melt" ); + PrecacheParticleSystem( "xms_ornament_glitter" ); + PrecacheParticleSystem( "xms_ornament_smash_blue" ); + PrecacheParticleSystem( "xms_ornament_smash_red" ); + + PrecacheParticleSystem( "drg_pomson_muzzleflash" ); + PrecacheParticleSystem( "drg_pomson_impact" ); + PrecacheParticleSystem( "drg_pomson_impact_drain" ); + + PrecacheParticleSystem( "dxhr_arm_muzzleflash" ); + + PrecacheModel( "effects/beam001_red.vmt" ); + PrecacheModel( "effects/beam001_blu.vmt" ); + PrecacheModel( "effects/beam001_white.vmt" ); + + PrecacheScriptSound( "Weapon_Mantreads.Impact" ); + + // Precache footstep override sounds. + PrecacheScriptSound( "cleats_conc.StepLeft" ); + PrecacheScriptSound( "cleats_conc.StepRight" ); + PrecacheScriptSound( "cleats_dirt.StepLeft" ); + PrecacheScriptSound( "cleats_dirt.StepRight" ); + + PrecacheScriptSound( "xmas.jingle" ); + PrecacheScriptSound( "xmas.jingle_higher" ); + + PrecacheScriptSound( "PegLeg.StepRight" ); + + // Halloween + // Bombinomicon deaths + PrecacheParticleSystem( "bombinomicon_burningdebris" ); + PrecacheParticleSystem( "bombinomicon_burningdebris_halloween" ); + + PrecacheParticleSystem( "halloween_player_death_blue" ); + PrecacheParticleSystem( "halloween_player_death" ); + + PrecacheScriptSound( "Bombinomicon.Explode" ); + + PrecacheScriptSound( "Weapon_DRG_Wrench.Teleport" ); + PrecacheScriptSound( "Weapon_Pomson.Single" ); + PrecacheScriptSound( "Weapon_Pomson.SingleCrit" ); + PrecacheScriptSound( "Weapon_Pomson.Reload" ); + PrecacheScriptSound( "Weapon_Pomson.DrainedVictim" ); + + PrecacheScriptSound( "BlastJump.Whistle" ); + + PrecacheScriptSound( "Spy.TeaseVictim" ); + PrecacheScriptSound( "Demoman.CritDeath" ); + PrecacheScriptSound( "Heavy.Battlecry03" ); + + PrecacheModel( "models/effects/resist_shield/resist_shield.mdl" ); + + PrecacheModel( "models/props_mvm/mvm_revive_tombstone.mdl" ); + + PrecacheScriptSound( "General.banana_slip" ); // Used for SodaPopper Hype Jumps + +#ifdef STAGING_ONLY + PrecacheScriptSound( "RD.SpaceGravityTransition" ); +#endif // STAGING_ONLY + + PrecacheScriptSound( "Parachute_open" ); + PrecacheScriptSound( "Parachute_close" ); + +#ifdef STAGING_ONLY + PrecacheScriptSound( "WeaponDNAGun.Transform" ); + PrecacheParticleSystem( "spy_stolen_smoke_blue" ); + PrecacheParticleSystem( "spy_stolen_smoke_red" ); +#endif // STAGING_ONLY + + // precache the EOTL bomb cart replacements + PrecacheModel( "models/props_trainyard/bomb_eotl_blue.mdl" ); + PrecacheModel( "models/props_trainyard/bomb_eotl_red.mdl" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::Precache() +{ + VPROF_BUDGET( "CTFPlayer::Precache", VPROF_BUDGETGROUP_PLAYER ); + + /* + Note: All TFPlayer specific must go inside PrecacheTFPlayer() + This assumes that we're loading all resources for any TF player class + The reason is to safe performance because tons of string compares is very EXPENSIVE!!! + The most offending function is PrecacheGibsForModel which re-parsing through KeyValues every time it's called + If you have any question, come talk to me (Bank) + */ + PrecacheTFPlayer(); + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: Allow pre-frame adjustments on the player +//----------------------------------------------------------------------------- +ConVar sv_runcmds( "sv_runcmds", "1" ); +void CTFPlayer::PlayerRunCommand( CUserCmd *ucmd, IMoveHelper *moveHelper ) +{ + static bool bSeenSyncError = false; + VPROF( "CTFPlayer::PlayerRunCommand" ); + + if ( !sv_runcmds.GetInt() ) + return; + + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + m_Shared.CreateVehicleMove( gpGlobals->frametime, ucmd ); + } + else if ( IsTaunting() || m_Shared.InCond( TF_COND_HALLOWEEN_THRILLER ) ) + { + // For some taunts, it is critical that the player not move once they start + if ( !CanMoveDuringTaunt() ) + { + ucmd->forwardmove = 0; + ucmd->upmove = 0; + ucmd->sidemove = 0; + ucmd->viewangles = pl.v_angle; + } + + if ( tf_allow_taunt_switch.GetInt() == 0 && ucmd->weaponselect != 0 ) + { + ucmd->weaponselect = 0; + + // FIXME: The client will have predicted the weapon switch and have + // called Holster/Deploy which will make the wielded weapon + // invisible on their end. + } + } + + BaseClass::PlayerRunCommand( ucmd, moveHelper ); + + // try to play taunt remap on input after updating user command + if ( IsTaunting() && m_flNextAllowTauntRemapInputTime >= 0.f && m_flNextAllowTauntRemapInputTime <= gpGlobals->curtime ) + { + float flSceneDuration = PlayTauntRemapInputScene(); + if ( flSceneDuration > 0.f ) + { + m_flNextAllowTauntRemapInputTime = gpGlobals->curtime + flSceneDuration; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::IsReadyToPlay( void ) +{ + bool bRetVal = false; + + if ( GetTeamNumber() == TEAM_SPECTATOR && m_bArenaSpectator == true ) + return false; + + //============================================================================= + // HPE_BEGIN: + // [msmith] We don't want to say that the player is ready if they're still + // a training video. + //============================================================================= + if ( TFGameRules() && TFGameRules()->IsInTraining() && tf_training_client_message.GetInt() == TRAINING_CLIENT_MESSAGE_WATCHING_INTRO_MOVIE ) + return false; + //============================================================================= + // HPE_END + //============================================================================= + + if ( GetTeamNumber() > LAST_SHARED_TEAM ) + { + if ( GetDesiredPlayerClassIndex() > TF_CLASS_UNDEFINED ) + { + bRetVal = true; + } + else + { + if ( TFGameRules() && TFGameRules()->IsInArenaMode() && tf_arena_force_class.GetBool() == true && tf_arena_use_queue.GetBool() == true ) + { + bRetVal = true; + } + } + } + + return bRetVal; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::IsReadyToSpawn( void ) +{ + if ( IsClassMenuOpen() ) + { + return false; + } + +#ifdef TF_RAID_MODE + if ( GetTeamNumber() == TF_TEAM_RED ) + { + if ( TFGameRules()->IsRaidMode() || TFGameRules()->IsBossBattleMode() ) + { + // enemy bots never respawn - they are spawned by the population system + return false; + } + } +#endif // TF_RAID_MODE + + // Medic attached to marker - delay + if ( m_hReviveMarker && m_hReviveMarker->IsReviveInProgress() && ( StateGet() != TF_STATE_DYING ) ) + { + return false; + } + + // Map-makers can force players to have custom respawn times + if ( GetRespawnTimeOverride() != -1.f && gpGlobals->curtime < GetDeathTime() + GetRespawnTimeOverride() ) + 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 ) +{ + m_Shared.ResetScores(); + CTF_GameStats.ResetPlayerStats( this ); + RemoveNemesisRelationships(); + MannVsMachineStats_ResetPlayerEvents( this ); + + BaseClass::ResetScores(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InitialSpawn( void ) +{ + BaseClass::InitialSpawn(); + + m_AttributeManager.InitializeAttributes( this ); + m_AttributeManager.SetPlayer( this ); + m_AttributeList.SetManager( &m_AttributeManager ); + + SetWeaponBuilder( NULL ); + + ResetScores(); + StateEnter( TF_STATE_WELCOME ); + UpdateInventory( true ); + + ResetAccumulatedSentryGunDamageDealt(); + ResetAccumulatedSentryGunKillCount(); + ResetDamagePerSecond(); + + IGameEvent * event = gameeventmanager->CreateEvent( "player_initial_spawn" ); + if ( event ) + { + event->SetInt( "index", entindex() ); + gameeventmanager->FireEvent( event ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Request this player's inventories from the steam backend +//----------------------------------------------------------------------------- +void CTFPlayer::UpdateOnRemove( void ) +{ + BaseClass::UpdateOnRemove(); + +#if !defined(NO_STEAM) + m_Inventory.RemoveListener( this ); +#endif + +} + +//----------------------------------------------------------------------------- +// Purpose: Override Base ApplyAbsVelocityImpulse (BaseEntity) to apply potential item attributes +//----------------------------------------------------------------------------- +void CTFPlayer::ApplyAbsVelocityImpulse( const Vector &vecImpulse ) +{ + // Check for Attributes (mult_aiming_knockback_resistance) + Vector vecForce = vecImpulse; + float flImpulseScale = 1.0f; + if ( IsPlayerClass( TF_CLASS_SNIPER ) && m_Shared.InCond( TF_COND_AIMING ) ) + { + CALL_ATTRIB_HOOK_FLOAT( flImpulseScale, mult_aiming_knockback_resistance ); + } + + if ( m_Shared.InCond( TF_COND_HALLOWEEN_TINY ) && !m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + flImpulseScale *= 2.f; + } + + // take extra force if you have a parachute deployed in x-y directions + if ( m_Shared.InCond( TF_COND_PARACHUTE_DEPLOYED ) ) + { + // don't allow parachute robot to get push in MvM + float flHorizontalScale = TFGameRules()->IsMannVsMachineMode() && IsBot() ? 0.f : 1.5f; + vecForce.x *= flHorizontalScale; + vecForce.y *= flHorizontalScale; + } + + CBaseMultiplayerPlayer::ApplyAbsVelocityImpulse( vecForce * flImpulseScale ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ApplyAirBlastImpulse( const Vector &vecImpulse ) +{ + // Knockout powerup carriers are immune to airblast + if ( m_Shared.GetCarryingRuneType() == RUNE_KNOCKOUT || m_Shared.InCond( TF_COND_MEGAHEAL ) ) + return; + + Vector vForce = vecImpulse; + + float flScale = 1.0f; + CALL_ATTRIB_HOOK_FLOAT( flScale, airblast_vulnerability_multiplier ); + vForce *= flScale; + + // if on the ground, require min force to boost you off it + if ( ( GetFlags() & FL_ONGROUND ) && ( vForce.z < JUMP_MIN_SPEED ) ) + { + // Minimum value of vecForce.z + vForce.z = JUMP_MIN_SPEED; + } + + CALL_ATTRIB_HOOK_FLOAT( vForce.z, airblast_vertical_vulnerability_multiplier ); + + RemoveFlag( FL_ONGROUND ); + m_Shared.AddCond( TF_COND_KNOCKED_INTO_AIR ); + + ApplyAbsVelocityImpulse( vForce ); +} + +//----------------------------------------------------------------------------- +// Purpose: Go between for Setting Local Punch Impulses. Checks item attributes +// Use this instead of directly calling m_Local.m_vecPunchAngle.SetX( value ); +//----------------------------------------------------------------------------- +bool CTFPlayer::ApplyPunchImpulseX ( float flImpulse ) +{ + // Check for No Aim Flinch + bool bFlinch = true; + if ( IsPlayerClass( TF_CLASS_SNIPER ) && m_Shared.InCond( TF_COND_AIMING ) ) + { + CTFWeaponBase *pWeapon = GetActiveTFWeapon(); + if ( pWeapon && WeaponID_IsSniperRifle( pWeapon->GetWeaponID() ) ) + { + CTFSniperRifle *pRifle = static_cast< CTFSniperRifle* >( pWeapon ); + if ( pRifle->IsFullyCharged() ) + { + int iAimingNoFlinch = 0; + CALL_ATTRIB_HOOK_INT( iAimingNoFlinch, aiming_no_flinch ); + if ( iAimingNoFlinch > 0 ) + { + bFlinch = false; + } + } + } + } + + if ( bFlinch ) + { + m_Local.m_vecPunchAngle.SetX( flImpulse ); + } + + return bFlinch; +} + +//----------------------------------------------------------------------------- +// Purpose: Request this player's inventories from the steam backend +//----------------------------------------------------------------------------- +void CTFPlayer::UpdateInventory( bool bInit ) +{ +#if !defined(NO_STEAM) + if ( IsFakeClient() ) + return; + + if ( bInit || !m_Inventory.GetSOC() ) + { + if ( steamgameserverapicontext->SteamGameServer() ) + { + CSteamID steamIDForPlayer; + if ( GetSteamID( &steamIDForPlayer ) ) + { + TFInventoryManager()->SteamRequestInventory( &m_Inventory, steamIDForPlayer, this ); + } + } + } + + // If we have an SOCache, we've got a connection to the GC + bool bInvalid = true; + if ( m_Inventory.GetSOC() ) + { + bInvalid = m_Inventory.GetSOC()->BIsInitialized() == false; + } + m_Shared.SetLoadoutUnavailable( bInvalid ); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Requests that the GC confirm that this player is supposed to have +// an SO cache on this gameserver and send it again if so. +//----------------------------------------------------------------------------- +void CTFPlayer::VerifySOCache() +{ +#if !defined(NO_STEAM) + if ( IsFakeClient() || IsHLTV() || IsReplay() ) + return; + + CSteamID steamIDForPlayer; + GetSteamID( &steamIDForPlayer ); + + if( steamIDForPlayer.BIndividualAccount() ) + { + // if we didn't find an inventory ask the GC to refresh us + GCSDK::CGCMsg<MsgGCVerifyCacheSubscription_t> msgVerifyCache( k_EMsgGCVerifyCacheSubscription ); + msgVerifyCache.Body().m_ulSteamID = steamIDForPlayer.ConvertToUint64(); + GCClientSystem()->BSendMessage( msgVerifyCache ); + } + else + { + Msg( "Cannot verify load for invalid steam ID %s\n", steamIDForPlayer.Render() ); + } +#endif +} + +#ifdef DEBUG +CON_COMMAND_F( verifyloadout, "Cause the server to verify the player's items on the server.", FCVAR_NONE ) +{ + CTFPlayer *pPlayer = ToTFPlayer( UTIL_GetCommandClient() ); + if ( !pPlayer ) + return; + + pPlayer->VerifySOCache(); +} +#endif // DEBUG + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTFPlayer::ShouldTransmit( const CCheckTransmitInfo *pInfo ) +{ + // always send information to student or client + if ( pInfo->m_pClientEnt ) + { + if ( m_hStudent && m_hStudent == CBaseEntity::Instance( pInfo->m_pClientEnt ) ) + { + return FL_EDICT_ALWAYS; + } + else if ( m_hCoach && m_hCoach == CBaseEntity::Instance( pInfo->m_pClientEnt ) ) + { + return FL_EDICT_ALWAYS; + } + else if ( TFGameRules() && TFGameRules()->IsPasstimeMode() ) + { + // TODO it should be possible to restrict this further based on + // the values of tf_passtime_player_reticles_friends/enemies + return FL_EDICT_ALWAYS; + } + + CBaseEntity *pRecipientEntity = CBaseEntity::Instance( pInfo->m_pClientEnt ); + if ( pRecipientEntity && pRecipientEntity->ShouldForceTransmitsForTeam( GetTeamNumber() ) ) + return FL_EDICT_ALWAYS; + } + + return BaseClass::ShouldTransmit( pInfo ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::SetupVisibility( CBaseEntity *pViewEntity, unsigned char *pvs, int pvssize ) +{ + // coach can only "see" what the student "sees" + if ( m_bIsCoaching && m_hStudent ) + { + Vector org; + org = m_hStudent->EyePosition(); + + engine->AddOriginToPVS( org ); + } + else + { + BaseClass::SetupVisibility( pViewEntity, pvs, pvssize ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::Spawn() +{ + VPROF_BUDGET( "CTFPlayer::Spawn", VPROF_BUDGETGROUP_PLAYER ); + MDLCACHE_CRITICAL_SECTION(); + + m_bIsABot = IsBot(); + + if ( m_bIsABot && IsBotOfType( TF_BOT_TYPE ) ) + { + m_nBotSkill = ToTFBot( this )->GetDifficulty(); + } + else + { + m_nBotSkill = 0; + } + + m_flSpawnTime = gpGlobals->curtime; + + SetModelScale( 1.0f ); + UpdateModel(); + + SetMoveType( MOVETYPE_WALK ); + BaseClass::Spawn(); + + // We have to clear this early, so that the sword knows its max health in ManageRegularWeapons below + m_Shared.SetDecapitations( 0 ); + + // Check the make sure we have our inventory each time we spawn + UpdateInventory( false ); + +#ifndef NO_STEAM + if( m_Shared.IsLoadoutUnavailable() ) + { + VerifySOCache(); + } +#endif + + // 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. + bool bMatchSummary = TFGameRules() && TFGameRules()->ShowMatchSummary(); + if ( m_Shared.InState( TF_STATE_ACTIVE ) || bMatchSummary ) + { + // remove our disguise each time we spawn + if ( m_Shared.InCond( TF_COND_DISGUISED ) ) + { + m_Shared.RemoveDisguise(); + } + + if ( !bMatchSummary ) + { + EmitSound( "Player.Spawn" ); + } + InitClass(); + m_Shared.RemoveAllCond(); // Remove conc'd, burning, rotting, hallucinating, etc. + + // add team glows for a period of time after we respawn + m_Shared.AddCond( TF_COND_TEAM_GLOWS, tf_spawn_glows_duration.GetInt() ); + + UpdateSkin( GetTeamNumber() ); + + // 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. + StopTaunt(); + + // 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 ) + { + if ( TFGameRules() && TFGameRules()->IsPasstimeMode() ) + { + CSingleUserRecipientFilter filter( this ); + TFGameRules()->SendHudNotification( filter, HUD_NOTIFY_PASSTIME_HOWTO ); + } + + 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.SetScoutEnergyDrinkMeter( 100.0f ); + m_Shared.SetScoutHypeMeter( 0.0f ); + m_Shared.StopScoutHypeDrain(); + m_Shared.SetRageMeter( 0.0f ); + m_Shared.SetDemomanChargeMeter( 100.0f ); + + m_Shared.ClearDamageEvents(); + m_AchievementData.ClearHistories(); + + m_flLastDamageTime = 0.f; + m_flLastDamageDoneTime = 0.f; + m_iMaxSentryKills = 0; + + m_flNextVoiceCommandTime = gpGlobals->curtime; + m_iVoiceSpamCounter = 0; + + ClearZoomOwner(); + SetFOV( this , 0 ); + + SetViewOffset( GetClassEyeHeight() * GetModelScale() ); + + RemoveAllScenesInvolvingActor( this ); + ClearExpression(); + m_flNextSpeakWeaponFire = gpGlobals->curtime; + + m_bInPowerPlay = false; + + // 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 ); + + m_iLeftGroundHealth = -1; + m_iBlastJumpState = 0; + m_bGoingFeignDeath = false; + m_bTakenBlastDamageSinceLastMovement = false; + + ClearTauntAttack(); + m_hTauntItem = NULL; + + m_bArenaIsAFK = false; + + m_Shared.SetFeignDeathReady( false ); + + m_bScattergunJump = false; + m_iOldStunFlags = 0; + + m_flAccumulatedHealthRegen = 0; + memset( m_flAccumulatedAmmoRegens, 0, sizeof(m_flAccumulatedAmmoRegens) ); + + IGameEvent * event = gameeventmanager->CreateEvent( "player_spawn" ); + if ( event ) + { + event->SetInt( "userid", GetUserID() ); + event->SetInt( "team", GetTeamNumber() ); + event->SetInt( "class", GetPlayerClass()->GetClassIndex() ); + + gameeventmanager->FireEvent( event ); + } + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() && GetTeamNumber() == TF_TEAM_BLUE ) + { + // raiders respawn invulnerable for a short time + m_Shared.AddCond( TF_COND_INVULNERABLE, tf_raid_respawn_safety_time.GetFloat() ); + + // friends glow + AddGlowEffect(); + } + + if ( TFGameRules()->IsBossBattleMode() && GetTeamNumber() == TF_TEAM_BLUE ) + { + // respawn invulnerable for a short time + m_Shared.AddCond( TF_COND_INVULNERABLE, tf_boss_battle_respawn_safety_time.GetFloat() ); + } +#endif // TF_RAID_MODE + + m_bIsMissionEnemy = false; + m_bIsSupportEnemy = false; + m_bIsLimitedSupportEnemy = false; + + m_Shared.Spawn(); + + m_bCollideWithSentry = false; + m_calledForMedicTimer.Invalidate(); + m_placedSapperTimer.Invalidate(); + + m_bIsReadyToHighFive = false; + + m_nForceTauntCam = 0; + m_bAllowedToRemoveTaunt = true; + + m_purgatoryPainMultiplier = 1; + m_purgatoryPainMultiplierTimer.Invalidate(); + + m_bIsTeleportingUsingEurekaEffect = false; + + m_playerMovementStuckTimer.Invalidate(); + + m_bIsMiniBoss = false; + m_bUseBossHealthBar = false; + + m_hGrapplingHookTarget = NULL; + m_nHookAttachedPlayers = 0; + m_bUsingActionSlot = false; + + m_flInspectTime = 0.f; + + m_flSendPickupWeaponMessageTime = -1.f; + + SetRespawnOverride( -1.f, NULL_STRING ); + + // Remove all powerups and add temporary invuln on spawn + if ( TFGameRules()->IsPowerupMode() ) + { + m_Shared.AddCond( TF_COND_INVULNERABLE_USER_BUFF, 8.f ); + } + + if ( TFGameRules() ) + { + // It's halloween, and it's hell time. Force this player to spawn in hell. + if ( TFGameRules()->ArePlayersInHell() ) + { + const char *pSpawnEntName = NULL; + if ( TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_HIGHTOWER ) ) + { + pSpawnEntName = "hell_ghost_spawn"; + } + else if ( TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) && CTFMinigameLogic::GetMinigameLogic() ) + { + CTFMiniGame *pActiveMinigame = CTFMinigameLogic::GetMinigameLogic()->GetActiveMinigame(); + if ( pActiveMinigame ) + { + pSpawnEntName = pActiveMinigame->GetTeamSpawnPointName( GetTeamNumber() ); + } + } + + if ( pSpawnEntName ) + { + TFGameRules()->SpawnPlayerInHell( this, pSpawnEntName ); + } + } + TFGameRules()->OnPlayerSpawned( this ); + } + + if ( m_hReviveMarker ) + { + UTIL_Remove( m_hReviveMarker ); + m_hReviveMarker = NULL; + } + + // make sure we clear custom attributes that we added + RemoveAllCustomAttributes(); + +#ifdef STAGING_ONLY + // in MVM, scout can see glowing cash by default + if ( TFGameRules()->IsMannVsMachineMode() ) + { + int iClass = GetPlayerClass()->GetClassIndex(); + if ( iClass == TF_CLASS_SCOUT || iClass == TF_CLASS_SPY ) + { + AddCustomAttribute( "mvm see cash through wall", 1.f ); + } + } +#endif + + CTFPlayerResource *pResource = dynamic_cast<CTFPlayerResource *>( g_pPlayerResource ); + if ( pResource ) + { + pResource->SetPlayerClassWhenKilled( entindex(), TF_CLASS_UNDEFINED ); + } + + if ( TFGameRules()->State_Get() == GR_STATE_BETWEEN_RNDS ) + { + const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( TFGameRules()->GetCurrentMatchGroup() ); + if ( pMatchDesc && pMatchDesc->m_params.m_bAutoReady ) + { + TFGameRules()->PlayerReadyStatus_UpdatePlayerState( this, true ); + } + } + + CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch(); + if ( pMatch ) + { + CSteamID steamID; + GetSteamID( &steamID ); + + // This client entered a running match + CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( steamID ); + if ( pMatchPlayer && TFGameRules() && TFGameRules()->State_Get() == GR_STATE_RND_RUNNING ) + { + pMatchPlayer->bPlayed = true; + } + } +} + +//----------------------------------------------------------------------------- +// 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 ) + { + bool bRemove = false; + + if ( TFGameRules()->IsInArenaMode() == true ) + { + if ( GetTeamNumber() != TEAM_SPECTATOR ) + { + if ( InSameTeam( pTemp ) == true ) + { + bRemove = true; + } + } + + if ( IsDisconnecting() == true ) + { + bRemove = true; + } + } + else + { + bRemove = true; + } + + if ( bRemove == true ) + { + // set this player to be not dominating anyone else + m_Shared.SetPlayerDominated( pTemp, false ); + m_iNumberofDominations = 0; + + // set no one else to be dominating this player + bool bThisPlayerIsDominatingMe = m_Shared.IsPlayerDominatingMe( i ); + pTemp->m_Shared.SetPlayerDominated( this, false ); + if ( bThisPlayerIsDominatingMe ) + { + int iDoms = pTemp->GetNumberofDominations(); + pTemp->SetNumberofDominations( iDoms - 1); + } + } + } + } + + if ( TFGameRules()->IsInArenaMode() == false || IsDisconnecting() == true ) + { + // reset the matrix of who has killed whom with respect to this player + CTF_GameStats.ResetKillHistory( this ); + } + + IGameEvent *event = gameeventmanager->CreateEvent( "remove_nemesis_relationships" ); + if ( event ) + { + event->SetInt( "player", entindex() ); + gameeventmanager->FireEvent( event ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::Regenerate( bool bRefillHealthAndAmmo /*= true*/ ) +{ + // We may have been boosted over our max health. If we have, + // restore it after we reset out class values. + int nOldMaxHealth = GetMaxHealth(); + int nOldHealth = GetHealth(); + bool bBoosted = ( nOldHealth > nOldMaxHealth || !bRefillHealthAndAmmo ) && ( nOldMaxHealth > 0 ); + + int nAmmo[ TF_AMMO_COUNT ]; + if ( !bRefillHealthAndAmmo ) + { + for ( int iAmmo = 0; iAmmo < TF_AMMO_COUNT; ++iAmmo ) + { + nAmmo[ iAmmo ] = GetAmmoCount( iAmmo ); + } + } + + m_bRegenerating = true; + // This recomputes MaxHealth + InitClass(); + m_bRegenerating = false; + + if ( bBoosted ) + { + SetHealth( MAX( nOldHealth, GetMaxHealth() ) ); + } + + if ( bRefillHealthAndAmmo ) + { + if ( m_Shared.InCond( TF_COND_BURNING ) ) + { + m_Shared.RemoveCond( TF_COND_BURNING ); + } + + if ( m_Shared.InCond( TF_COND_URINE ) ) + { + m_Shared.RemoveCond( TF_COND_URINE ); + } + + if ( m_Shared.InCond( TF_COND_MAD_MILK ) ) + { + m_Shared.RemoveCond( TF_COND_MAD_MILK ); + } + + if ( m_Shared.InCond( TF_COND_BLEEDING ) ) + { + m_Shared.RemoveCond( TF_COND_BLEEDING ); + } + + if ( m_Shared.InCond( TF_COND_ENERGY_BUFF ) ) + { + m_Shared.RemoveCond( TF_COND_ENERGY_BUFF ); + + if ( m_Shared.InCond( TF_COND_CANNOT_SWITCH_FROM_MELEE ) ) + { + m_Shared.RemoveCond( TF_COND_CANNOT_SWITCH_FROM_MELEE ); + } + } + + if ( m_Shared.InCond( TF_COND_PHASE ) ) + { + m_Shared.RemoveCond( TF_COND_PHASE ); + } + + if ( m_Shared.InCond( TF_COND_PARACHUTE_DEPLOYED ) ) + { + m_Shared.RemoveCond( TF_COND_PARACHUTE_DEPLOYED ); + } + + if ( m_Shared.InCond( TF_COND_PLAGUE ) ) + { + m_Shared.RemoveCond( TF_COND_PLAGUE ); + } + +#ifdef STAGING_ONLY + if ( m_Shared.InCond( TF_COND_TRANQ_MARKED ) ) + { + m_Shared.RemoveCond( TF_COND_TRANQ_MARKED ); + } +#endif // STAGING_ONLY + + m_Shared.SetSpyCloakMeter( 100.0f ); + m_Shared.SetScoutEnergyDrinkMeter( 100.0f ); + m_Shared.SetDemomanChargeMeter( 100.0f ); + } + + // Reset our first allowed fire time. This allows honorbound weapons to be switched away + // from for a bit. + m_Shared.m_flFirstPrimaryAttack = MAX( m_Shared.m_flFirstPrimaryAttack, gpGlobals->curtime + 1.0f ); + + if ( bRefillHealthAndAmmo ) + { + for ( int iAmmo = 0; iAmmo < TF_AMMO_COUNT; ++iAmmo ) + { + if ( GetAmmoCount( iAmmo ) > GetMaxAmmo( iAmmo ) ) + { + SetAmmoCount( GetMaxAmmo( iAmmo ), iAmmo ); + } + } + } + else + { + for ( int iAmmo = 0; iAmmo < TF_AMMO_COUNT; ++iAmmo ) + { + SetAmmoCount( nAmmo[ iAmmo ], iAmmo ); + } + } + + IGameEvent *event = gameeventmanager->CreateEvent( "player_regenerate" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InitClass( void ) +{ + 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(); + + // Set initial health and armor based on class. + // Do it after items have been delivered, so items can modify it + SetMaxHealth( GetMaxHealth() ); + SetHealth( GetMaxHealth() ); + + TeamFortress_SetSpeed(); + +#ifdef STAGING_ONLY + int nForceRobotModel = 0; + CALL_ATTRIB_HOOK_INT( nForceRobotModel, appear_as_mvm_robot ); + if ( nForceRobotModel != 0 ) + { + int nClassIndex = ( GetPlayerClass() ? GetPlayerClass()->GetClassIndex() : TF_CLASS_UNDEFINED ); + GetPlayerClass()->SetCustomModel( g_szPlayerRobotModels[nClassIndex], USE_CLASS_ANIMATIONS ); + UpdateModel(); + SetBloodColor( DONT_BLEED ); + } + else + { + GetPlayerClass()->SetCustomModel( NULL ); + UpdateModel(); + SetBloodColor( BLOOD_COLOR_RED ); + } +#endif +} + +//----------------------------------------------------------------------------- +// 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(); + if ( GetTeamNumber() == TEAM_SPECTATOR ) + { + RemoveAllWeapons(); + return; + } + + // Give weapons. + ManageRegularWeapons( pData ); + + if ( !TFGameRules() || !TFGameRules()->IsInMedievalMode() ) + { + // Give a builder weapon for each object the playerclass is allowed to build + ManageBuilderWeapons( pData ); + } + + // Weapons that added greater ammo than base require us to now fill the player up to max ammo + for ( int iAmmo = 0; iAmmo < TF_AMMO_COUNT; ++iAmmo ) + { + GiveAmmo( GetMaxAmmo(iAmmo), iAmmo, true, kAmmoSource_Resupply ); + } + + // Clear the player's banner buffs. + m_Shared.RemoveCond( TF_COND_OFFENSEBUFF ); + m_Shared.RemoveCond( TF_COND_DEFENSEBUFF ); + m_Shared.RemoveCond( TF_COND_REGENONDAMAGEBUFF ); + m_Shared.RemoveCond( TF_COND_NOHEALINGDAMAGEBUFF ); + m_Shared.RemoveCond( TF_COND_DEFENSEBUFF_NO_CRIT_BLOCK ); + m_Shared.RemoveCond( TF_COND_DEFENSEBUFF_HIGH ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ManageBuilderWeapons( TFPlayerClassData_t *pData ) +{ + // Collect all builders and validate them against the list of objects (below) + CUtlVector< CTFWeaponBuilder* > vecBuilderDestroyList; + for ( int i = 0; i < MAX_WEAPONS; ++i ) + { + CTFWeaponBuilder *pBuilder = dynamic_cast< CTFWeaponBuilder* >( GetWeapon( i ) ); + if ( !pBuilder ) + continue; + + vecBuilderDestroyList.AddToTail( pBuilder ); + } + + CEconItemView *pLoadoutBuilderItemView = NULL; + + // Go through each object and see if we need to create or remove builders + for ( int i = 0; i < OBJ_LAST; ++i ) + { + if ( !GetPlayerClass()->CanBuildObject( i ) ) + continue; + + // TODO: Need to add support for "n" builders, rather hard-wired for two. + // Currently, the only class that uses more than one is the spy: + // - BUILDER is OBJ_ATTACHMENT_SAPPER, which is invoked via weapon selection (see objects.txt). + // - BUILDER2 is OBJ_SPY_TRAP, which is invoked via a build command from PDA3 (spy-specific). +#ifdef STAGING_ONLY + int nLoadoutPos = ( GetObjectInfo( i )->m_bRequiresOwnBuilder ) ? LOADOUT_POSITION_BUILDING2 : LOADOUT_POSITION_BUILDING; +#else + int nLoadoutPos = LOADOUT_POSITION_BUILDING; +#endif + pLoadoutBuilderItemView = GetLoadoutItem( GetPlayerClass()->GetClassIndex(), nLoadoutPos, true ); + + // Do we have a specific builder for this object? + CTFWeaponBuilder *pBuilder = CTFPlayerSharedUtils::GetBuilderForObjectType( this, i ); + if ( pBuilder ) + { + // We may have a different builder back-end item now. If so, destroy and make a new one below. + CEconItemView *pCurrentBuilderItemView = pBuilder->GetAttributeContainer()->GetItem(); + if ( pCurrentBuilderItemView == NULL || pLoadoutBuilderItemView == NULL || !ItemsMatch( pData, pCurrentBuilderItemView, pLoadoutBuilderItemView, pBuilder ) ) + { + // Manually nuke the item from the weapon list here so that we don't find it + vecBuilderDestroyList.FindAndRemove( pBuilder ); + Weapon_Detach( pBuilder ); + UTIL_Remove( pBuilder ); + + // Wrong builder item, so pretend we didn't find one + pBuilder = NULL; + } + } + else if ( !GetObjectInfo( i )->m_bRequiresOwnBuilder ) + { + // Do we have a default builder, and an object that doesn't require a specific builder? + pBuilder = CTFPlayerSharedUtils::GetBuilderForObjectType( this, -1 ); + if ( pBuilder ) + { + // Flag it as supported by this builder (ugly, but necessary for legacy system) + pBuilder->SetObjectTypeAsBuildable( i ); + } + } + + // Is a new builder required? + if ( !pBuilder || ( GetObjectInfo( i )->m_bRequiresOwnBuilder && !( CTFPlayerSharedUtils::GetBuilderForObjectType( this, i ) ) ) ) + { + pBuilder = dynamic_cast< CTFWeaponBuilder* >( GiveNamedItem( "tf_weapon_builder", i, pLoadoutBuilderItemView ) ); + if ( pBuilder ) + { + pBuilder->DefaultTouch( this ); + } + } + + // Builder settings + if ( pBuilder ) + { + if ( m_bRegenerating == false ) + { + pBuilder->WeaponReset(); + } + + pBuilder->GiveDefaultAmmo(); + pBuilder->ChangeTeam( GetTeamNumber() ); + pBuilder->SetObjectTypeAsBuildable( i ); + pBuilder->m_nSkin = GetTeamNumber() - 2; // color the w_model to the team + + // Pull it out of the "destroy" list + vecBuilderDestroyList.FindAndRemove( pBuilder ); + } + } + + // Anything left should be destroyed + FOR_EACH_VEC( vecBuilderDestroyList, i ) + { + Assert( vecBuilderDestroyList[i] ); + + Weapon_Detach( vecBuilderDestroyList[i] ); + UTIL_Remove( vecBuilderDestroyList[i] ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::ItemsMatch( TFPlayerClassData_t *pData, CEconItemView *pCurWeaponItem, CEconItemView *pNewWeaponItem, CTFWeaponBase *pWpnEntity ) +{ + if ( !pNewWeaponItem || !pNewWeaponItem->IsValid() ) + return false; + + // If we already have a weapon in this slot but is not the same type, nuke it (changed classes) + // We don't need to do this for non-base items because they've already been verified above. + bool bHasNonBaseWeapon = pNewWeaponItem ? pNewWeaponItem->GetItemQuality() != AE_NORMAL : false; + if ( bHasNonBaseWeapon ) + { + // If the item isn't the one we're supposed to have, nuke it + if ( pCurWeaponItem->GetItemID() != pNewWeaponItem->GetItemID() ) + { + /* + Msg("Removing %s because its global index (%d) doesn't match the loadout's (%d)\n", pWeapon->GetDebugName(), + pCurWeaponItem->GetItemID(), + pNewWeaponItem->GetItemID() ); + */ + return false; + } + + // Some items create different entities when wielded by different classes. If so, we need to say + // the items don't match so the item gets recreated as the right entity. + if ( pWpnEntity ) + { + const char *pszCurWeaponClass = pWpnEntity->GetClassname(), + *pszNewWeaponTransClass = TranslateWeaponEntForClass( pNewWeaponItem->GetStaticData()->GetItemClass(), GetPlayerClass()->GetClassIndex() ); + + if ( !pszCurWeaponClass || !pszNewWeaponTransClass || Q_stricmp( pszCurWeaponClass, pszNewWeaponTransClass ) ) + return false; + } + } + else + { + if ( pCurWeaponItem->GetItemQuality() != AE_NORMAL || (pCurWeaponItem->GetItemDefIndex() != pNewWeaponItem->GetItemDefIndex()) ) + { + //Msg("Removing %s because it's not the right type for the class.\n", pWeapon->GetDebugName() ); + return false; + } + + CSteamID ownerSteamID; + GetSteamID( &ownerSteamID ); + + // If the owner is not the same, then they're different as well. This catches + // cases of stock items comparing + if ( pCurWeaponItem->GetAccountID() != ownerSteamID.GetAccountID() ) + { + return false; + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::ItemIsAllowed( CEconItemView *pItem ) +{ + if ( !pItem || !pItem->GetStaticData() ) + return false; + + int iClass = GetPlayerClass()->GetClassIndex(); + int iSlot = pItem->GetStaticData()->GetLoadoutSlot(iClass); + + // Passtime hack to allow passtime gun + if ( V_stristr( pItem->GetItemDefinition()->GetDefinitionName(), "passtime" ) ) + { + return TFGameRules() && TFGameRules()->IsPasstimeMode(); + } + + // Holiday Restriction + CEconItemDefinition* pData = pItem->GetStaticData(); + if ( TFGameRules() && pData && pData->GetHolidayRestriction() ) + { + int iHolidayRestriction = UTIL_GetHolidayForString( pData->GetHolidayRestriction() ); + if ( iHolidayRestriction != kHoliday_None && !TFGameRules()->IsHolidayActive( iHolidayRestriction ) ) + return false; + } + + if ( TFGameRules()->InStalemate() && mp_stalemate_meleeonly.GetBool() ) + { + bool bMeleeOnlyAllowed = (iSlot == LOADOUT_POSITION_MELEE) + || (iClass == TF_CLASS_SPY && (iSlot == LOADOUT_POSITION_PDA || iSlot == LOADOUT_POSITION_PDA2)); + + if ( !bMeleeOnlyAllowed ) + return false; + } + + if ( TFGameRules()->IsInMedievalMode() ) + { + bool bMedievalModeAllowed = false; + + // Allow all melee-class weapons, non-weapons, and the spy equipment. + switch ( iSlot ) + { + case LOADOUT_POSITION_MELEE: + case LOADOUT_POSITION_HEAD: + case LOADOUT_POSITION_MISC: + case LOADOUT_POSITION_MISC2: + case LOADOUT_POSITION_ACTION: + case LOADOUT_POSITION_TAUNT: + case LOADOUT_POSITION_TAUNT2: + case LOADOUT_POSITION_TAUNT3: + case LOADOUT_POSITION_TAUNT4: + case LOADOUT_POSITION_TAUNT5: + case LOADOUT_POSITION_TAUNT6: + case LOADOUT_POSITION_TAUNT7: + case LOADOUT_POSITION_TAUNT8: +#ifdef STAGING_ONLY + case LOADOUT_POSITION_PDA3: + case LOADOUT_POSITION_BUILDING2: +#endif + bMedievalModeAllowed = true; + break; + + case LOADOUT_POSITION_PDA: + case LOADOUT_POSITION_PDA2: + if ( iClass == TF_CLASS_SPY ) + bMedievalModeAllowed = true; + break; + } + + if ( !bMedievalModeAllowed ) + { + static CSchemaAttributeDefHandle pAttrib_AllowedInMedievalMode( "allowed in medieval mode" ); + if ( !pItem->FindAttribute( pAttrib_AllowedInMedievalMode ) ) + { + return false; + } + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ManageRegularWeapons( TFPlayerClassData_t *pData ) +{ + // Reset ammo. + RemoveAllAmmo(); + + // Remove our disguise weapon. + m_Shared.RemoveDisguiseWeapon(); + + CUtlVector<const char *> precacheStrings; + + CBaseCombatWeapon* pCurrentWeapon = m_hActiveWeapon; + + // 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( GetMaxAmmo(iAmmo), iAmmo, true, kAmmoSource_Resupply ); + } + + if ( IsX360() ) + { + ManageRegularWeaponsLegacy( pData ); + } + else + { + // Loop through our current wearables and ensure we're supposed to have them. + ValidateWearables( pData ); + + // Loop through all our current weapons, and ensure we're supposed to have them. + ValidateWeapons( pData, true ); + + // Create a copy of currently equipped items, if we equip something new report player loadout + bool bItemsChanged = false; + + // Now Loop through our inventory for the current class, and give us any items we don't have. + int iClass = GetPlayerClass()->GetClassIndex(); + if ( iClass > TF_CLASS_UNDEFINED && iClass < TF_CLASS_COUNT ) + { + CSteamID ownerSteamID; + GetSteamID( &ownerSteamID ); + + for ( int i = 0; i < CLASS_LOADOUT_POSITION_COUNT; i++ ) + { + // bots don't need the action slot item for MvM (canteen) + if ( ( i == LOADOUT_POSITION_ACTION ) && IsBot() && TFGameRules() && TFGameRules()->IsMannVsMachineMode() && ( GetTeamNumber() == TF_TEAM_PVE_INVADERS ) ) + continue; + + m_EquippedLoadoutItemIndices[i] = LOADOUT_SLOT_USE_BASE_ITEM; + + // use base items in training mode + CEconItemView *pItem = GetLoadoutItem( iClass, i, true ); + if ( !pItem || !pItem->IsValid() ) + continue; + + if ( !ItemIsAllowed( pItem ) ) + continue; + + // Only do this for taunts, because other items will be caught by the dynamic model loading system. + if ( IsTauntSlot( i ) ) + { + tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s - Precaching taunts, etc", __FUNCTION__ ); + // This has to be done before the continue for "no_entity", because we're trying to precache taunts which + // explicitly bail out there. + precacheStrings.RemoveAll(); + pItem->GetItemDefinition()->GeneratePrecacheModelStrings( false, &precacheStrings ); + FOR_EACH_VEC( precacheStrings, iModel ) + { + if ( precacheStrings[iModel] && ( *precacheStrings[iModel] ) ) + { + PrecacheModel( precacheStrings[iModel], false ); + } + } + } + + m_EquippedLoadoutItemIndices[i] = pItem->GetItemID(); + + Assert( pItem->GetStaticData()->GetItemClass() ); + if ( pItem->GetStaticData()->GetItemClass() && FStrEq( pItem->GetStaticData()->GetItemClass(), "no_entity" ) ) + continue; + + CTFWeaponBase *pCurrentWeaponOfType = NULL; + bool bAlreadyHave = false; + // Don't need to check weapons if it's a wearable-only slot + if ( !IsWearableSlot(i) || pItem->GetItemDefinition()->IsActingAsAWeapon() ) + { + // Weapon slot. Check out weapons to see if we have it. + for ( int wpn = 0; wpn < MAX_WEAPONS; wpn++ ) + { + CTFWeaponBase *pWeapon = (CTFWeaponBase *)GetWeapon(wpn); + if ( !pWeapon ) + continue; + + if ( ItemsMatch( pData, pWeapon->GetAttributeContainer()->GetItem(), pItem, pWeapon ) ) + { + pCurrentWeaponOfType = pWeapon; + bAlreadyHave = true; + break; + } + } + } + + CEconWearable *pWearable = NULL; + if ( !bAlreadyHave ) + { + // We couldn't find a matching weapon. See if we have a matching wearable. + for ( int wbl = 0; wbl < m_hMyWearables.Count(); wbl++ ) + { + pWearable = m_hMyWearables[wbl]; + if ( !pWearable ) + continue; + + CEconItemView *pWearableView = pWearable->GetAttributeContainer()->GetItem(); + if ( ItemsMatch( pData, pWearableView, pItem ) ) + { + bAlreadyHave = true; + break; + } + } + } + + if ( !bAlreadyHave && pItem->GetStaticData()->GetItemClass() ) + { + CEconEntity *pNewItem = dynamic_cast<CEconEntity*>(GiveNamedItem( pItem->GetStaticData()->GetItemClass(), 0, pItem )); + Assert( pNewItem ); + if ( pNewItem ) + { + pNewItem->GetAttributeContainer()->GetItem()->SetOverrideAccountID( ownerSteamID.GetAccountID() ); + + CTFWeaponBuilder *pBuilder = dynamic_cast<CTFWeaponBuilder*>( (CBaseEntity*)pNewItem ); + if ( pBuilder ) + { + pBuilder->SetSubType( pData->m_aBuildable[0] ); + } + + CBaseCombatWeapon* pWeapon = dynamic_cast< CBaseCombatWeapon* >( pNewItem ); + if ( pWeapon ) + { + pWeapon->SetSoundsEnabled( false ); + } + + pNewItem->GiveTo( this ); + + if ( pWeapon ) + { + pWeapon->SetSoundsEnabled( true ); + } + } + } + else + { + if ( pCurrentWeaponOfType ) + { + pCurrentWeaponOfType->UpdateExtraWearables(); + + // We need to ensure all hands pointers are updated for all weapons. + // Otherwise we could end up using animation sequences from the wrong class hands. + pCurrentWeaponOfType->UpdateHands(); + } + } + + bItemsChanged |= !bAlreadyHave; + } // For each item in load out + } + + if ( bItemsChanged ) + { + CTF_GameStats.Event_PlayerLoadoutChanged( this, false ); + } + // We may have added weapons that make others invalid. Recheck. + ValidateWeapons( pData, false ); + + if ( m_hActiveWeapon.Get() != pCurrentWeapon && m_hActiveWeapon ) + { + m_hActiveWeapon->WeaponSound( DEPLOY ); + } + + CSingleUserRecipientFilter filter( this ); + UserMessageBegin( filter, "PlayerLoadoutUpdated" ); + WRITE_BYTE( entindex() ); + MessageEnd(); + } + + + // On equip, legacy source code will autoswitch to new weapons. + // Instead of refactoring, we check here to see if we are allowed to have certain weapons switched to + + // TF2: Not allowed to have a actionslot item as last or active on regenerate / respawn + // HACK Don't allow the parachute to be an active weapon + CTFWeaponBase *pCurr = GetActiveTFWeapon(); + CTFWeaponBase *pPrev = dynamic_cast<CTFWeaponBase*>( GetLastWeapon() ); + if ( ( pCurr && pCurr->GetAttributeContainer()->GetItem()->GetEquippedPositionForClass( GetPlayerClass()->GetClassIndex() ) == LOADOUT_POSITION_ACTION ) + || ( pPrev && pPrev->GetAttributeContainer()->GetItem()->GetEquippedPositionForClass( GetPlayerClass()->GetClassIndex() ) == LOADOUT_POSITION_ACTION ) + || ( pCurr && pCurr->GetWeaponID() == TF_WEAPON_PARACHUTE ) + ) { + m_bRegenerating = false; + m_iLastWeaponSlot = 0; + } + + if ( m_bRegenerating == false ) + { + bool bWepSwitched = false; + if ( m_bRememberActiveWeapon && m_iActiveWeaponTypePriorToDeath ) + { + CTFWeaponBase *pWeapon = Weapon_OwnsThisID( m_iActiveWeaponTypePriorToDeath ); + if ( pWeapon && pWeapon->GetAttributeContainer()->GetItem()->GetEquippedPositionForClass( GetPlayerClass()->GetClassIndex() ) != LOADOUT_POSITION_ACTION ) + { + bWepSwitched = Weapon_Switch( pWeapon ); + } + } + + if ( !bWepSwitched ) + { + SetActiveWeapon( NULL ); + + // Find a weapon to switch to, starting with primary. + CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase*>( GetEntityForLoadoutSlot( LOADOUT_POSITION_PRIMARY ) ); + if ( !pWeapon || !pWeapon->CanBeSelected() || !Weapon_Switch( pWeapon ) ) + { + pWeapon = dynamic_cast<CTFWeaponBase*>( GetEntityForLoadoutSlot( LOADOUT_POSITION_SECONDARY ) ); + if ( !pWeapon || pWeapon->CanBeSelected() || !Weapon_Switch( pWeapon ) ) + { + pWeapon = dynamic_cast<CTFWeaponBase*>( GetEntityForLoadoutSlot( LOADOUT_POSITION_MELEE ) ); + Weapon_Switch( pWeapon ); + } + } + } + + if ( (m_iLastWeaponSlot == 0 || !m_bRememberLastWeapon) && !m_bRememberActiveWeapon ) + { + m_iLastWeaponSlot = 1; + } + + if ( !Weapon_GetSlot( m_iLastWeaponSlot ) ) + { + Weapon_SetLast( Weapon_GetSlot( TF_WPN_TYPE_MELEE ) ); + } + else + { + Weapon_SetLast( Weapon_GetSlot( m_iLastWeaponSlot ) ); + } + } + + // Now make sure we don't have too much ammo. This can happen if an item has reduced our max ammo. + for ( int iAmmo = 0; iAmmo < TF_AMMO_COUNT; ++iAmmo ) + { + int iMax = GetMaxAmmo(iAmmo); + if ( iMax < GetAmmoCount(iAmmo) ) + { + RemoveAmmo( GetAmmoCount(iAmmo) - iMax, iAmmo ); + } + } + + // If our max health dropped below current due to item changes, drop our current health. + // If we're not being buffed, clamp it to max. Otherwise, clamp it to the max buffed health + int iMaxHealth = m_Shared.InCond( TF_COND_HEALTH_BUFF ) ? m_Shared.GetMaxBuffedHealth() : GetMaxHealth(); + if ( m_iHealth > iMaxHealth ) + { + // Modify health manually to prevent showing all the "you got hurt" UI. + m_iHealth = iMaxHealth; + } + + if ( TFGameRules()->InStalemate() && mp_stalemate_meleeonly.GetBool() ) + { + CBaseCombatWeapon *meleeWeapon = Weapon_GetSlot( TF_WPN_TYPE_MELEE ); + if ( meleeWeapon ) + { + Weapon_Switch( meleeWeapon ); + } + } + + // In testing mode, switch bots to the weapon being tested + if ( TFGameRules()->IsInItemTestingMode() && IsFakeClient() ) + { + // Our first player should be the human tester + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( 1 ) ); + if ( pPlayer && !pPlayer->IsFakeClient() ) + { + // Loop through all the items we're testing + FOR_EACH_VEC( pPlayer->m_ItemsToTest, i ) + { + CEconItemView *pItem = &pPlayer->m_ItemsToTest[i].scriptItem; + if ( !pItem ) + continue; + + int iSlot = pItem->GetStaticData()->GetLoadoutSlot( GetPlayerClass()->GetClassIndex() ); + if ( IsWearableSlot( iSlot ) ) + continue; + + CBaseCombatWeapon *pWeapon = Weapon_GetSlot( iSlot ); + if ( pWeapon ) + { + Weapon_Switch( pWeapon ); + break; + } + } + } + } + + if ( TFGameRules() && TFGameRules()->IsPVEModeActive() && !IsBot() ) + { + if ( m_Inventory.ClassLoadoutHasChanged( GetPlayerClass()->GetClassIndex() ) + || ( m_bSwitchedClass ) + || ( g_pPopulationManager && g_pPopulationManager->IsRestoringCheckpoint() ) ) + { + ReapplyPlayerUpgrades(); + } + + // Calculate how much money is being used on active class / items + int nSpending = 0; + int iClass = GetPlayerClass()->GetClassIndex(); + CUtlVector< CUpgradeInfo > *upgrades = g_pPopulationManager->GetPlayerUpgradeHistory( this ); + if ( upgrades ) + { + for( int u = 0; u < upgrades->Count(); ++u ) + { + // Class Match, Check to see if we have this item equipped + if ( iClass == upgrades->Element(u).m_iPlayerClass) + { + // Player upgrade + if ( upgrades->Element( u ).m_itemDefIndex == INVALID_ITEM_DEF_INDEX ) + { + nSpending += upgrades->Element(u).m_nCost; + continue; + } + + // Item upgrade, look at equipment only not miscs or bottle + for ( int itemIndex = 0; itemIndex <= LOADOUT_POSITION_PDA2; itemIndex++ ) + { + CEconItemView *pItem = GetLoadoutItem( iClass, itemIndex, true ); + if ( upgrades->Element(u).m_itemDefIndex == pItem->GetItemDefIndex() ) + { + nSpending += upgrades->Element(u).m_nCost; + break; + } + } + } + } + } + + CMannVsMachineStats *pStats = MannVsMachineStats_GetInstance(); + if ( pStats ) + { + pStats->NotifyPlayerActiveUpgradeCosts( this, nSpending ); + } + } + + PostInventoryApplication(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEconItemView *CTFPlayer::GetLoadoutItem( int iClass, int iSlot, bool bReportWhitelistFails ) +{ + if ( TFGameRules()->IsInItemTestingMode() ) + { + CEconItemView *pItem = ItemTesting_GetTestItem( iClass, iSlot ); + if ( pItem ) + return pItem; + } + + if ( TFGameRules()->IsInTraining() || TFGameRules()->IsInItemTestingMode() ) + { + CTFInventoryManager *pInventoryManager = TFInventoryManager(); + return pInventoryManager->GetBaseItemForClass( iClass, iSlot ); + } + + CEconItemView *pItem = m_Inventory.GetItemInLoadout( iClass, iSlot ); + + // Check to see if this item passes the tournament rules (in whitelist/or normal quality). + // If it doesn't, we fall back to the base item for the loadout slot. + if ( (pItem && pItem->IsValid()) && (pItem->GetItemQuality() != AE_NORMAL) && !pItem->GetStaticData()->IsAllowedInMatch() && TFGameRules()->IsInTournamentMode() ) + { + if ( bReportWhitelistFails ) + { + ClientPrint( this, HUD_PRINTNOTIFY, "#Item_BlacklistedInMatch", pItem->GetStaticData()->GetItemBaseName() ); + } + + pItem = TFInventoryManager()->GetBaseItemForClass( iClass, iSlot ); + } + + return pItem; +} + +//----------------------------------------------------------------------------- +// Purpose: Handles pressing the use action slot item key. +//----------------------------------------------------------------------------- +void CTFPlayer::UseActionSlotItemPressed( void ) +{ + m_bUsingActionSlot = true; + + if ( TryToPickupDroppedWeapon() ) + return; + + int iNoiseMaker = 0; + CALL_ATTRIB_HOOK_INT( iNoiseMaker, enable_misc2_noisemaker ); + if ( iNoiseMaker ) + { + DoNoiseMaker(); + return; + } + + CBaseEntity *pActionSlotEntity = GetEntityForLoadoutSlot( LOADOUT_POSITION_ACTION ); + if ( !pActionSlotEntity ) + return; + + // get the equipped item and see what it is + CTFPowerupBottle *pPowerupBottle = dynamic_cast< CTFPowerupBottle* >( pActionSlotEntity ); + if ( pPowerupBottle ) + { + // @todo send event to clients so that they know what's going on + pPowerupBottle->Use(); + return; + } + + // is it a throwable? + CTFThrowable *pThrowable = dynamic_cast< CTFThrowable* >( pActionSlotEntity ); + if ( pThrowable ) + { + if ( !Weapon_ShouldSelectItem( pThrowable ) ) + return; + + if ( GetActiveWeapon() ) + { + if ( !GetActiveWeapon()->CanHolster() ) + return; + + ResetAutoaim( ); + } + + // Check if this is the spellbook so we can save off info to preserve weapon switching + CTFSpellBook *pSpellBook = dynamic_cast< CTFSpellBook* >( pThrowable ); + if ( pSpellBook ) + { + if ( !pSpellBook->CanCastSpell( this ) ) + { + // if no spell force a roll if cheat is active + if ( tf_halloween_unlimited_spells.GetBool() && !pSpellBook->HasASpellWithCharges() ) + { + pSpellBook->RollNewSpell( 0 ); + } + else if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + // if I'm in a halloween Vehicle, cast the spell immediately + //pSpellBook->CastKartSpell(); + pSpellBook->PrimaryAttack(); + } + else + { + EmitSound_t params; + params.m_flSoundTime = 0; + params.m_pflSoundDuration = 0; + params.m_pSoundName = "Player.DenyWeaponSelection"; + + CSingleUserRecipientFilter filter( this ); + EmitSound( filter, entindex(), params ); + } + return; + } + // Notify the spellbook of the current last used weapon + pSpellBook->SaveLastWeapon( GetLastWeapon() ); + } + // Equip it + Weapon_Switch( pThrowable ); + return; + } + + if ( TFGameRules() && TFGameRules()->IsUsingGrapplingHook() ) + { + CTFGrapplingHook *pGrapplingHook = dynamic_cast< CTFGrapplingHook* >( pActionSlotEntity ); + if ( pGrapplingHook ) + { + Weapon_Switch( pGrapplingHook ); + return; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handles releasing the use action slot item key. +//----------------------------------------------------------------------------- +void CTFPlayer::UseActionSlotItemReleased( void ) +{ + m_bUsingActionSlot = false; + + if ( TFGameRules() && TFGameRules()->IsUsingGrapplingHook() ) + { + // if we're using the hook, switch back to the last weapon + if ( GetActiveTFWeapon() && GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_GRAPPLINGHOOK ) + { + CBaseCombatWeapon *pLastWeapon = GetLastWeapon(); + if ( pLastWeapon && Weapon_CanSwitchTo( pLastWeapon ) ) + { + Weapon_Switch( pLastWeapon ); + } + else + { + // in case we failed to switch back to last weapon for some reason, just find the next best + SwitchToNextBestWeapon( pLastWeapon ); + } + + return; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handles pressing the inspect key. +//----------------------------------------------------------------------------- +void CTFPlayer::InspectButtonPressed() +{ + m_flInspectTime = gpGlobals->curtime; +} + +//----------------------------------------------------------------------------- +// Purpose: Handles releasing the inspect key. +//----------------------------------------------------------------------------- +void CTFPlayer::InspectButtonReleased() +{ + m_flInspectTime = 0.f; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::AddToSpyKnife( float value, bool force ) +{ + CTFKnife *pWpn = (CTFKnife *)Weapon_OwnsThisID( TF_WEAPON_KNIFE ); + if ( !pWpn ) + return false; + + return pWpn->DecreaseRegenerationTime( value, force ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveAllItems() +{ + // Nuke items. + for ( int i = 0; i < MAX_WEAPONS; i++ ) + { + CTFWeaponBase *pWeapon = (CTFWeaponBase *)GetWeapon(i); + if ( !pWeapon ) + continue; + + Weapon_Detach( pWeapon ); + UTIL_Remove( pWeapon ); + } + + // Nuke wearables. + for ( int wbl = m_hMyWearables.Count()-1; wbl >= 0; wbl-- ) + { + CEconWearable *pWearable = m_hMyWearables[wbl]; + Assert( pWearable ); + if ( !pWearable ) + continue; + + RemoveWearable( pWearable ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ValidateWeapons( TFPlayerClassData_t *pData, bool bResetWeapons ) +{ + CSteamID steamIDForPlayer; + GetSteamID( &steamIDForPlayer ); + + bool bFoundBuffItem = false; + + bool bOverrideRemoval = false; + if ( bResetWeapons && m_bForceItemRemovalOnRespawn ) + { + bOverrideRemoval = true; + m_bForceItemRemovalOnRespawn = false; + } + + // Disable sounds for all weapons. We're about to switch weapons MANY times, + // and we don't want the deploy sounds to play for any of them, since none + // of the deploys are actually visible to the player + for ( int i = 0; i < MAX_WEAPONS; i++ ) + { + CTFWeaponBase *pWeapon = (CTFWeaponBase *)GetWeapon(i); + if ( !pWeapon ) + continue; + + pWeapon->SetSoundsEnabled( false ); + } + + for ( int i = 0; i < MAX_WEAPONS; i++ ) + { + CTFWeaponBase *pWeapon = (CTFWeaponBase *)GetWeapon(i); + if ( !pWeapon ) + continue; + + int iLoadoutSlot = pWeapon->GetAttributeContainer()->GetItem()->GetStaticData()->GetLoadoutSlot( GetPlayerClass()->GetClassIndex() ); + CEconItemView *pItem = GetLoadoutItem( GetPlayerClass()->GetClassIndex(), iLoadoutSlot ); + + // See if gamerules says this item isn't allowed right now + bool bForceRemoved = bOverrideRemoval || !ItemIsAllowed( pItem ); + + if ( bForceRemoved || !ItemsMatch( pData, pWeapon->GetAttributeContainer()->GetItem(), pItem, pWeapon ) ) + { + // we can't hold this weapon anymore, switch to the next best weapon before removing it + if ( GetActiveTFWeapon() == pWeapon ) + { + SwitchToNextBestWeapon( pWeapon ); + } + + // drop weapon that belongs to other player, unless we're not regenerating + // which happens at round restart + if ( !bForceRemoved && m_bRegenerating ) + { + CEconItemView *pDroppedItem = pWeapon->GetAttributeContainer()->GetItem(); + CSteamID steamID; + GetSteamID( &steamID ); + if ( pDroppedItem->GetAccountID() != steamID.GetAccountID() ) + { + // Find the position and angle of the weapons so the "ammo box" matches. + Vector vecPackOrigin; + QAngle vecPackAngles; + if( !CalculateAmmoPackPositionAndAngles( pWeapon, vecPackOrigin, vecPackAngles ) ) + return; + + CTFDroppedWeapon *pDroppedWeapon = CTFDroppedWeapon::Create( this, vecPackOrigin, vecPackAngles, pWeapon->GetWorldModel(), pDroppedItem ); + if ( pDroppedWeapon ) + { + pDroppedWeapon->InitDroppedWeapon( this, pWeapon, false ); + } + } + } + + // We shouldn't have this weapon. Remove it. + Weapon_Detach( pWeapon ); + UTIL_Remove( pWeapon ); + } + else if ( bResetWeapons ) + { + // We should have this weapon. Reset it. + pWeapon->ChangeTeam( GetTeamNumber() ); + pWeapon->GiveDefaultAmmo(); + pWeapon->ClearKillComboCount(); + + if ( m_bRegenerating == false ) + { + pWeapon->WeaponReset(); + } + else + { + pWeapon->WeaponRegenerate(); + } + } + + int nBuffType = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, nBuffType, set_buff_type ); + + if ( pWeapon->GetWeaponID() == TF_WEAPON_BUFF_ITEM || nBuffType ) + { + bFoundBuffItem = true; + } + } + + // Reenable sounds for all weapons + for ( int i = 0; i < MAX_WEAPONS; i++ ) + { + CTFWeaponBase *pWeapon = (CTFWeaponBase *)GetWeapon(i); + if ( !pWeapon ) + continue; + + pWeapon->SetSoundsEnabled( true ); + } + + // Prevent a rage exploit with changing items outside of a spawn room + if ( ( IsPlayerClass( TF_CLASS_SOLDIER ) || IsPlayerClass( TF_CLASS_PYRO ) || IsPlayerClass( TF_CLASS_SNIPER ) ) && !bFoundBuffItem ) + { + m_Shared.SetRageMeter( 0.0f ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ValidateWearables( TFPlayerClassData_t *pData ) +{ + CSteamID steamIDForPlayer; + GetSteamID( &steamIDForPlayer ); + + bool bIsDisguisedSpy = IsPlayerClass( TF_CLASS_SPY ) && m_Shared.InCond( TF_COND_DISGUISED ); + + // Need to move backwards because we'll be removing them as we find them. + for ( int wbl = m_hMyWearables.Count()-1; wbl >= 0; wbl-- ) + { + CEconWearable *pWearable = m_hMyWearables[wbl]; + Assert( pWearable ); + if ( !pWearable ) + { + // Integrity is failing, remove NULLs + m_hMyWearables.Remove( wbl ); + continue; + } + + CTFWearable *pTFWearable = assert_cast< CTFWearable* >( pWearable ); + if ( bIsDisguisedSpy && pTFWearable->IsDisguiseWearable() ) + continue; + + bool itemMatch = false; + + // If you are an extra wearable, just make sure your associated weapon is valid instead + CBaseEntity *pEntity = pTFWearable->GetWeaponAssociatedWith(); + if ( pEntity ) + { + CTFWeaponBase *pWeapon = assert_cast< CTFWeaponBase* >( pTFWearable->GetWeaponAssociatedWith() ); + + int iLoadoutSlot = pWeapon->GetAttributeContainer()->GetItem()->GetStaticData()->GetLoadoutSlot( GetPlayerClass()->GetClassIndex() ); + if (iLoadoutSlot >= 0 ) + { + CEconItemView *pItem = TFInventoryManager()->GetItemInLoadoutForClass( GetPlayerClass()->GetClassIndex(), iLoadoutSlot, &steamIDForPlayer ); + itemMatch |= ItemsMatch( pData, pWeapon->GetAttributeContainer()->GetItem(), pItem ); + } + } + else + { + // Regular Wearable + int iLoadoutSlot = pWearable->GetAttributeContainer()->GetItem()->GetStaticData()->GetLoadoutSlot( GetPlayerClass()->GetClassIndex() ); + if ( iLoadoutSlot >= 0 ) + { + CEconItemView *pItem = TFInventoryManager()->GetItemInLoadoutForClass( GetPlayerClass()->GetClassIndex(), iLoadoutSlot, &steamIDForPlayer ); + itemMatch |= ItemsMatch( pData, pWearable->GetAttributeContainer()->GetItem(), pItem ); + + // Item says what slot it wants to be in, but Misc's and Taunts can be in multiple places, check against all + bool bLoadoutMisc = iLoadoutSlot == LOADOUT_POSITION_MISC; + bool bLoadoutTaunt = iLoadoutSlot == LOADOUT_POSITION_TAUNT; + if ( bLoadoutMisc || bLoadoutTaunt ) + { + for ( int i = LOADOUT_POSITION_INVALID + 1; i < CLASS_LOADOUT_POSITION_COUNT; i++ ) + { + if ( ( bLoadoutMisc && IsMiscSlot( i ) ) || ( bLoadoutTaunt && IsTauntSlot( i ) ) ) + { + pItem = TFInventoryManager()->GetItemInLoadoutForClass( GetPlayerClass()->GetClassIndex(), i, &steamIDForPlayer ); + itemMatch |= ItemsMatch( pData, pWearable->GetAttributeContainer()->GetItem(), pItem ); + } + } + } + } + } + + if ( !itemMatch || pWearable->GetTeamNumber() != GetTeamNumber() || m_bForceItemRemovalOnRespawn || m_bSwitchedClass ) + { + if ( !pWearable->AlwaysAllow() ) + { + // We shouldn't have this wearable. Remove it. + RemoveWearable( pWearable ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::PostInventoryApplication( void ) +{ + m_Shared.RecalculatePlayerBodygroups(); + + if ( m_Shared.InCond( TF_COND_DISGUISED ) ) + { + // Using weapons lockers destroys our disguise weapon, so we might need a new one. + m_Shared.DetermineDisguiseWeapon( false ); + } + + // Apply set bonuses. + ApplySetBonuses(); + + // Remove our disguise if we can't disguise. + if ( !CanDisguise() ) + { + RemoveDisguise(); + } + + // Notify the client. + IGameEvent *event = gameeventmanager->CreateEvent( "post_inventory_application" ); + if ( event ) + { + event->SetInt( "userid", GetUserID() ); + gameeventmanager->FireEvent( event ); + } + + // Iterate over all of our wearables + int iPlayerSkinOverride = 0; + for ( int i=0; i< GetNumWearables(); ++i ) + { + CTFWearable *pWearable = dynamic_cast<CTFWearable *>( GetWearable( i ) ); + if ( pWearable == NULL || pWearable->IsDisguiseWearable() ) + continue; + + // Check if we have an item that activates the skin override we want + // find first skin override + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWearable, iPlayerSkinOverride, player_skin_override ); + if ( iPlayerSkinOverride != 0 ) // Zombie + { + break; + } + } + m_iPlayerSkinOverride = iPlayerSkinOverride; + + m_Inventory.ClearClassLoadoutChangeTracking(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ManageRegularWeaponsLegacy( TFPlayerClassData_t *pData ) +{ + CSteamID steamIDForPlayer; + GetSteamID( &steamIDForPlayer ); + + 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 = WeaponIdToAlias( iWeaponID ); + + CTFWeaponBase *pWeapon = (CTFWeaponBase *)GetWeapon( iWeapon ); + + WEAPON_FILE_INFO_HANDLE hWpnInfo = LookupWeaponInfoSlot( pszWeaponName ); + Assert( hWpnInfo != GetInvalidWeaponInfoHandle() ); + CTFWeaponInfo *pWeaponInfo = dynamic_cast<CTFWeaponInfo*>( GetFileWeaponInfoFromHandle( hWpnInfo ) ); + int iLoadoutSlot = pWeaponInfo->m_iWeaponType; + + // HACK: Convert engineer's second PDA to using the second pda slot + if ( iWeaponID == TF_WEAPON_PDA_ENGINEER_DESTROY || iWeaponID == TF_WEAPON_INVIS ) + { + iLoadoutSlot = LOADOUT_POSITION_PDA2; + } + +#ifdef STAGING_ONLY + if ( iWeaponID == TF_WEAPON_PDA_SPY_BUILD ) + { + iLoadoutSlot = LOADOUT_POSITION_PDA3; + } +#endif + + // Do we have a custom weapon in this slot? + CEconItemView *pItem = TFInventoryManager()->GetItemInLoadoutForClass( GetPlayerClass()->GetClassIndex(), iLoadoutSlot, &steamIDForPlayer ); + bool bHasNonBaseWeapon = pItem ? pItem->GetItemQuality() != AE_NORMAL : false; + + if ( pWeapon ) + { + bool bShouldRemove = false; + + if ( pItem ) + { + // If the item isn't the one we're supposed to have, nuke it + if ( pWeapon->GetAttributeContainer()->GetItem()->GetItemID() != pItem->GetItemID() ) + { + bShouldRemove = true; + + /* + Msg("Removing %s because its global index (%d) doesn't match the loadout's (%d)\n", pWeapon->GetDebugName(), + pWeapon->GetAttributeContainer()->GetItem()->GetItemID(), + pItem->GetItemID() ); + */ + } + } + else + { + // We should have a base item in our loadout. + if ( pWeapon->GetAttributeContainer()->GetItem()->GetItemQuality() != AE_NORMAL ) + { + bShouldRemove = true; + //Msg("Removing %s because it's a non-base item, and the loadout specifies a base item.\n", pWeapon->GetDebugName() ); + } + } + + // If we already have a weapon in this slot but is not the same type, nuke it (changed classes) + // We don't do this if the weapon in this slot isn't a base item, because items like the flaregun + // don't have matching weaponIDs, yet they shouldn't be removed. The inventory system has already + // ensured that the weapon is valid in this slot. + if ( !bShouldRemove && pWeapon->GetWeaponID() != iWeaponID && !bHasNonBaseWeapon ) + { + bShouldRemove = true; + //Msg("Removing %s because it's not the right type for the class.\n", pWeapon->GetDebugName() ); + } + + if ( bShouldRemove ) + { + Weapon_Detach( pWeapon ); + UTIL_Remove( pWeapon ); + pWeapon = NULL; + } + } + + if ( !bHasNonBaseWeapon ) + { + pWeapon = dynamic_cast<CTFWeaponBase*>(Weapon_OwnsThisID( iWeaponID )); + } + + if ( pWeapon ) + { + Assert( pWeapon->GetAttributeContainer()->GetItem()->GetItemID() == ( pItem ? pItem->GetItemID() : INVALID_ITEM_ID ) ); + + pWeapon->ChangeTeam( GetTeamNumber() ); + pWeapon->GiveDefaultAmmo(); + + if ( m_bRegenerating == false ) + { + pWeapon->WeaponReset(); + } + + //char tempstr[1024]; + //g_pVGuiLocalize->ConvertUnicodeToANSI( pWeapon->GetAttributeContainer()->GetItem()->GetItemName(), tempstr, sizeof(tempstr) ); + //Msg("Updated %s for %s\n", tempstr, GetPlayerName() ); + } + else + { + CEconEntity* pNewItem = dynamic_cast<CEconEntity*>(GiveNamedItem( pszWeaponName, 0, pItem )); + Assert( pNewItem ); + if ( pNewItem ) + { + //char tempstr[1024]; + //g_pVGuiLocalize->ConvertUnicodeToANSI( pWeapon->GetAttributeContainer()->GetItem()->GetItemName(), tempstr, sizeof(tempstr) ); + //Msg("Created %s for %s\n", tempstr, GetPlayerName() ); + //pWeapon->DebugDescribe(); + + pNewItem->GiveTo( 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 we lack a primary or secondary weapon, start with our melee weapon ready. + // This is for supporting new unlockables that take up weapon slots and leave our character with nothing to wield. + int iMainWeaponCount = 0; + CTFWeaponBase* pMeleeWeapon = NULL; + for ( int i = 0; i < MAX_WEAPONS; i++ ) + { + CTFWeaponBase *pWeapon = (CTFWeaponBase*) GetWeapon(i); + + if ( pWeapon == NULL ) + continue; + + if ( pWeapon->GetTFWpnData().m_iWeaponType == TF_WPN_TYPE_PRIMARY || + pWeapon->GetTFWpnData().m_iWeaponType == TF_WPN_TYPE_SECONDARY ) + { + ++iMainWeaponCount; + } + else if ( pWeapon->GetTFWpnData().m_iWeaponType == TF_WPN_TYPE_MELEE ) + { + pMeleeWeapon = pWeapon; + } + } + if ( pMeleeWeapon && (iMainWeaponCount==0) ) + { + Weapon_Switch( pMeleeWeapon ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Create and give the named item to the player. Then return it. +//----------------------------------------------------------------------------- +CBaseEntity *CTFPlayer::GiveNamedItem( const char *pszName, int iSubType, const CEconItemView *pScriptItem, bool bForce ) +{ + // We need to support players putting any shotgun into a shotgun slot, pistol into a pistol slot, etc. + // For legacy reasons, different classes actually spawn different entities for their shotguns/pistols/etc. + // To deal with this, we translate entities into the right one for the class we're playing. + if ( !bForce ) + { + // We don't do this if force is set, since a spy might be disguising as this character, etc. + pszName = TranslateWeaponEntForClass( pszName, GetPlayerClass()->GetClassIndex() ); + } + + if ( !pszName ) + return NULL; + + // If I already own this type don't create one + if ( Weapon_OwnsThisType(pszName, iSubType) && !bForce) + { + Assert(0); + return NULL; + } + + CBaseEntity *pItem = NULL; + + if ( pScriptItem ) + { + // Generate a weapon directly from that item + pItem = ItemGeneration()->GenerateItemFromScriptData( pScriptItem, GetLocalOrigin(), vec3_angle, pszName ); + } + else + { + // Generate a base item of the specified type + CItemSelectionCriteria criteria; + criteria.SetQuality( AE_NORMAL ); + criteria.BAddCondition( "name", k_EOperator_String_EQ, pszName, true ); + pItem = ItemGeneration()->GenerateRandomItem( &criteria, GetAbsOrigin(), vec3_angle ); + } + + if ( pItem == NULL ) + { + Msg( "Failed to generate base item: %s\n", pszName ); + return NULL; + } + + pItem->AddSpawnFlags( SF_NORESPAWN ); + + CBaseCombatWeapon *pWeapon = dynamic_cast<CBaseCombatWeapon*>( (CBaseEntity*)pItem ); + if ( pWeapon ) + { + pWeapon->SetSubType( iSubType ); + } + + DispatchSpawn( pItem ); + + if ( pItem != NULL && !(pItem->IsMarkedForDeletion()) ) + { + pItem->Touch( this ); + } + + return pItem; +} + +//----------------------------------------------------------------------------- +// Purpose: Destroy all attributes on this player that match the bSetBonuses flag +//----------------------------------------------------------------------------- +void CTFPlayer::RemovePlayerAttributes( bool bSetBonuses ) +{ + const int iAttribs = m_AttributeList.GetNumAttributes(); + for ( int i = iAttribs-1; i >= 0; i-- ) + { + const CEconItemAttribute *pAttrib = m_AttributeList.GetAttribute(i); + const CEconItemAttributeDefinition *pAttrDef = GetItemSchema()->GetAttributeDefinition( pAttrib->GetAttribIndex() ); + if ( !pAttrDef || (pAttrDef->BIsSetBonusAttribute() == bSetBonuses) ) + { + m_AttributeList.RemoveAttributeByIndex( i ); + } + } + GetAttributeManager()->OnAttributeValuesChanged(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ApplySetBonuses( void ) +{ + RemovePlayerAttributes( true ); + + CUtlVector<const CEconItemSetDefinition *> pActiveSets; + GetActiveSets( &pActiveSets ); + + FOR_EACH_VEC( pActiveSets, set ) + { + for ( int i = 0; i < pActiveSets[set]->m_iAttributes.Count(); i++ ) + { + const CEconItemAttributeDefinition *pAttrDef = GetItemSchema()->GetAttributeDefinition( pActiveSets[set]->m_iAttributes[i].m_iAttribDefIndex ); + if ( pAttrDef ) + { + Assert( pAttrDef->GetAttributeType() ); + Assert( pAttrDef->GetAttributeType()->BSupportsGameplayModificationAndNetworking() ); // is an assert instead of a check because we're in client code here -- this means someone set up a set with bad data + Assert( pAttrDef->BIsSetBonusAttribute() ); + + float flAttrValue = pActiveSets[set]->m_iAttributes[i].m_flValue; + GetAttributeList()->SetRuntimeAttributeValue( pAttrDef, flAttrValue ); + } + } + } +} + +#ifdef TF_RAID_MODE +//----------------------------------------------------------------------------- +// Return true if the given entity can be used by a dead Raider +// as a respawn point in Raid mode. +bool IsValidRaidRespawnTarget( CBaseEntity *entity ) +{ + if ( !entity->IsPlayer() ) + { + CObjectSentrygun *pSentry = dynamic_cast< CObjectSentrygun* >( entity ); + if ( pSentry && pSentry->GetTeamNumber() == TF_TEAM_BLUE ) + { + if ( pSentry->GetOwner() && !pSentry->GetOwner()->IsBot() ) + { + return true; + } + } + + return false; + } + + if ( entity->GetTeamNumber() != TF_TEAM_BLUE ) + return false; + + CTFPlayer *player = ToTFPlayer( entity ); + CTFBot *bot = ToTFBot( player ); + return !bot || !bot->HasAttribute( CTFBot::IS_NPC ); +} +#endif // TF_RAID_MODE + +//----------------------------------------------------------------------------- +// Purpose: Find a spawn point for the player. +//----------------------------------------------------------------------------- +CBaseEntity* CTFPlayer::EntSelectSpawnPoint() +{ + CBaseEntity *pSpot = g_pLastSpawnPoints[ GetTeamNumber() ]; + const char *pSpawnPointName = ""; + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() ) + { + if ( GetTeamNumber() == TF_TEAM_BLUE ) + { + // only spawn next to friends if the round is not restarting + if ( TFGameRules()->State_Get() == GR_STATE_RND_RUNNING ) + { + if ( tf_raid_use_rescue_closets.GetBool() ) + { + // find a valid rescue closet to spawn into + CBaseEntity *rescueSpawn = g_pRaidLogic->GetRescueRespawn(); + + if ( rescueSpawn ) + { + return rescueSpawn; + } + } + else if ( tf_boss_battle_respawn_on_friends.GetBool() ) + { + // the raiders are in the wild - respawn next to them + float timeSinceInjured = -1.0f; + CBaseEntity *respawnEntity = NULL; + + // if we are observing a friend, spawn into them + CBaseEntity *watchEntity = GetObserverTarget(); + if ( watchEntity && IsValidRaidRespawnTarget( watchEntity ) ) + { + respawnEntity = watchEntity; + } + else + { + // spawn on the least recently damaged friend + CTeam *raidingTeam = GetGlobalTeam( TF_TEAM_BLUE ); + for( int i=0; i<raidingTeam->GetNumPlayers(); ++i ) + { + CTFPlayer *buddy = (CTFPlayer *)raidingTeam->GetPlayer(i); + + // we can't use IsAlive(), because that has already been reset since + // this code is mid-spawn. Use m_Shared state instead. + if ( buddy != this && buddy->m_Shared.InState( TF_STATE_ACTIVE ) && IsValidRaidRespawnTarget( buddy ) ) + { + // pick the friend who has been hurt least recently + if ( buddy->GetTimeSinceLastInjury( TF_TEAM_RED ) > timeSinceInjured ) + { + timeSinceInjured = buddy->GetTimeSinceLastInjury( TF_TEAM_RED ); + respawnEntity = buddy; + } + } + } + } + + if ( respawnEntity ) + { + CPVSFilter filter( respawnEntity->GetAbsOrigin() ); + TE_TFParticleEffect( filter, 0.0, "teleported_blue", respawnEntity->GetAbsOrigin(), vec3_angle ); + TE_TFParticleEffect( filter, 0.0, "player_sparkles_blue", respawnEntity->GetAbsOrigin(), vec3_angle, this, PATTACH_POINT ); + return respawnEntity; + } + } + } + } + } +#endif // TF_RAID_MODE + + bool bMatchSummary = TFGameRules() && TFGameRules()->ShowMatchSummary(); + + // See if the map is asking to force this player to spawn at a specific location + if ( GetRespawnLocationOverride() && !bMatchSummary ) + { + if ( SelectSpawnSpotByName( GetRespawnLocationOverride(), pSpot ) ) + { + m_pSpawnPoint = dynamic_cast< CTFTeamSpawn* >( pSpot ); // Is this even used anymore? + return pSpot; + } + + // If the entity doesn't exist - or isn't valid - let the regular system handle it + } + + switch( GetTeamNumber() ) + { + case TF_TEAM_RED: + case TF_TEAM_BLUE: + { + pSpawnPointName = "info_player_teamspawn"; + if ( SelectSpawnSpotByType( pSpawnPointName, pSpot ) ) + { + g_pLastSpawnPoints[ GetTeamNumber() ] = pSpot; + } + else if ( pSpot ) + { + int iClass = GetPlayerClass()->GetClassIndex(); + if ( iClass >= 0 && iClass < ARRAYSIZE( g_aPlayerClassNames ) ) + { + Warning( "EntSelectSpawnPoint(): No valid spawns for class %s on team %i found, even though at least one spawn entity exists.\n", g_aPlayerClassNames[iClass], GetTeamNumber() ); + } + } + + // need to save this for later so we can apply and modifiers to the armor and grenades...after the call to InitClass() + m_pSpawnPoint = dynamic_cast<CTFTeamSpawn*>( pSpot ); + break; + } + case TEAM_SPECTATOR: + case TEAM_UNASSIGNED: + default: + { + pSpot = CBaseEntity::Instance( INDEXENT(0) ); + break; + } + } + + if ( !pSpot ) + { + Warning( "PutClientInServer: no %s on level\n", pSpawnPointName ); + return CBaseEntity::Instance( INDEXENT(0) ); + } + + return pSpot; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::SelectSpawnSpotByType( const char *pEntClassName, CBaseEntity* &pSpot ) +{ + bool bMatchSummary = TFGameRules()->ShowMatchSummary(); + CBaseEntity *pMatchSummaryFallback = NULL; + + // 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 spawn point that's clear except for another players. We + // don't collide with our team members, so we should be fine. + bool bIgnorePlayers = false; + // When dealing with a standard spawn ent, try to obey any class spawn flags + bool bRestrictByClass = !V_strcmp( pEntClassName, "info_player_teamspawn" ); + + 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 ) ) + { + goto next_spawn_point; + } + // SpawnFlags were only recently added to the .fgd (Feb 2016), which means older maps won't have any flags at all (they default to on). + // So this means we only look for restrictions when we find flags, which a map compiled after this change would/should have. + else if ( bRestrictByClass && pSpot->GetSpawnFlags() ) + { + int nClass = GetPlayerClass()->GetClassIndex() - 1; + if ( !pSpot->HasSpawnFlags( ( 1 << nClass ) ) ) + { + goto next_spawn_point; + } + } + + // Found a valid spawn point. + return true; + } + } + + next_spawn_point:; + + // Let's save off a fallback spot for competitive mode + if ( bMatchSummary && !pMatchSummaryFallback ) + { + CTFTeamSpawn *pCTFSpawn = dynamic_cast<CTFTeamSpawn*>( pSpot ); + if ( pCTFSpawn ) + { + if ( ( pCTFSpawn->GetTeamNumber() == pCTFSpawn->GetTeamNumber() ) && ( pCTFSpawn->GetMatchSummaryType() == PlayerTeamSpawn_MatchSummary_None ) ) + { + pMatchSummaryFallback = pCTFSpawn; + } + } + } + + // Get the next spawning point to check. + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + + // Exhausted the list + if ( pSpot == pFirstSpot ) + { + // Loop through again, ignoring class restrictions (but check against players) + if ( bRestrictByClass ) + { + bRestrictByClass = false; + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + } + // Loop through again, ignoring players and classes + else if ( !bRestrictByClass && !bIgnorePlayers ) + { + bIgnorePlayers = true; + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + } + } + } + // Continue until a valid spawn point is found or we hit the start. + while ( pSpot != pFirstSpot ); + + // Return a fallback spot for competitive mode + if ( bMatchSummary && pMatchSummaryFallback ) + { + pSpot = pMatchSummaryFallback; + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: We're being asked to use a spawn with a specific name +//----------------------------------------------------------------------------- +bool CTFPlayer::SelectSpawnSpotByName( const char *pEntName, CBaseEntity* &pSpot ) +{ + if ( pEntName && pEntName[0] ) + { + pSpot = gEntList.FindEntityByName( pSpot, pEntName ); + + while ( pSpot ) + { + if ( TFGameRules()->IsSpawnPointValid( pSpot, this, true, PlayerTeamSpawnMode_Triggered ) ) + return true; + + pSpot = gEntList.FindEntityByName( pSpot, pEntName ); + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::DoAnimationEvent( PlayerAnimEvent_t event, int nData ) +{ + MDLCACHE_CRITICAL_SECTION(); + + m_PlayerAnimState->DoAnimationEvent( event, nData ); + TE_PlayerAnimEvent( this, event, nData ); // Send to any clients who can see this guy. +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::HandleAnimEvent( animevent_t *pEvent ) +{ + if ( pEvent->event == AE_TAUNT_ENABLE_MOVE ) + { + m_bAllowMoveDuringTaunt = true; + } + else if ( pEvent->event == AE_TAUNT_DISABLE_MOVE ) + { + m_bAllowMoveDuringTaunt = false; + } + else if ( pEvent->event == AE_WPN_HIDE ) + { + // does nothing for now. + } + else + BaseClass::HandleAnimEvent( pEvent ); +} + +//----------------------------------------------------------------------------- +// 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( int nPreferedTeam /*= TF_TEAM_AUTOASSIGN*/ ) +{ + int iTeam = TEAM_SPECTATOR; + + CTFTeam *pBlue = TFTeamMgr()->GetTeam( TF_TEAM_BLUE ); + CTFTeam *pRed = TFTeamMgr()->GetTeam( TF_TEAM_RED ); + + if ( pBlue && pRed ) + { + if ( TFGameRules() ) + { + if ( TFGameRules()->IsInHighlanderMode() ) + { + if ( ( pBlue->GetNumPlayers() >= TF_LAST_NORMAL_CLASS - 1 ) && + ( pRed->GetNumPlayers() >= TF_LAST_NORMAL_CLASS - 1 ) ) + { + // teams are full....join team Spectator for now + return TEAM_SPECTATOR; + } + } + + bool bReturnDefenders = false; + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsBossBattleMode() ) + { + bReturnDefenders = true; + } +#endif // TF_RAID_MODE + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + bReturnDefenders = true; + } + + if ( bReturnDefenders ) + { + // If joining a MVM game that's in-progress, give us the max per-player collected value + if ( TFGameRules()->IsMannVsMachineMode() && g_pPopulationManager ) + { + int nRoundCurrency = MannVsMachineStats_GetAcquiredCredits(); + nRoundCurrency += g_pPopulationManager->GetStartingCurrency(); + + // Check to see if this player has an upgrade history and apply it to them + // deduct any cash that has already been spent + int spentCurrency = g_pPopulationManager->GetPlayerCurrencySpent( this ); + + if ( m_nCurrency < nRoundCurrency ) + { + SetCurrency( nRoundCurrency - spentCurrency ); + } + + if ( g_pPopulationManager ) + { + // See if the team's earned any respec credits + if ( TFGameRules()->IsMannVsMachineRespecEnabled() && !g_pPopulationManager->GetNumRespecsAvailableForPlayer( this ) ) + { + uint16 nRespecs = g_pPopulationManager->GetNumRespecsEarned(); + if ( nRespecs ) + { + g_pPopulationManager->SetNumRespecsForPlayer( this, nRespecs ); + } + } + + // Set buyback credits - if they aren't reconnecting + if ( !g_pPopulationManager->IsPlayerBeingTrackedForBuybacks( this ) ) + { + g_pPopulationManager->SetBuybackCreditsForPlayer( this, tf_mvm_buybacks_per_wave.GetInt() ); + } + } + } + + return TFGameRules()->GetTeamAssignmentOverride( this, TF_TEAM_PVE_DEFENDERS ); + } + } + + CTFBot *pPlayerBot = dynamic_cast<CTFBot*>( this ); + if ( FStrEq( tf_bot_quota_mode.GetString(), "fill" ) && ( tf_bot_quota.GetInt() > 0 ) && !( pPlayerBot && pPlayerBot->HasAttribute( CTFBot::QUOTA_MANANGED ) ) ) + { + // We're using 'tf_bot_quota_mode fill' to keep the teams even so balance based on the human players on each team + int nPlayerCountRed = 0; + int nPlayerCountBlue = 0; + + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + + if ( pPlayer == NULL ) + continue; + + if ( FNullEnt( pPlayer->edict() ) ) + continue; + + if ( !pPlayer->IsConnected() ) + continue; + + if ( !pPlayer->IsPlayer() ) + continue; + + CTFBot* pBot = dynamic_cast<CTFBot*>( pPlayer ); + if ( pBot && pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) ) + continue; + + if ( pPlayer->GetTeamNumber() == TF_TEAM_RED ) + { + nPlayerCountRed++; + } + else if( pPlayer->GetTeamNumber() == TF_TEAM_BLUE ) + { + nPlayerCountBlue++; + } + } + + if ( nPlayerCountRed < nPlayerCountBlue ) + { + iTeam = TF_TEAM_RED; + } + else if ( nPlayerCountBlue < nPlayerCountRed ) + { + iTeam = TF_TEAM_BLUE; + } + else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT || pRed->GetRole() == TEAM_ROLE_DEFENDERS ) + { + // AutoTeam should give new players to the attackers on A/D maps if the teams are even + iTeam = TF_TEAM_BLUE; + } + else + { + // teams have an even number of human players, pick a random team + iTeam = RandomInt( 0, 1 ) ? TF_TEAM_RED : TF_TEAM_BLUE; + } + + bool bKick = false; + // Now we have a team we want to join to balance the human players, can we join it? + if ( iTeam == TF_TEAM_RED ) + { + if ( pBlue->GetNumPlayers() < pRed->GetNumPlayers() ) + { + bKick = true; + } + } + else + { + if ( pRed->GetNumPlayers() < pBlue->GetNumPlayers() ) + { + bKick = true; + } + } + + if ( !bKick || TheTFBots().RemoveBotFromTeamAndKick( iTeam ) ) + { + return iTeam; + } + + // If kick needed but failed, fall through to default logic + } + + if ( pBlue->GetNumPlayers() < pRed->GetNumPlayers() ) + { + iTeam = TF_TEAM_BLUE; + } + else if ( pRed->GetNumPlayers() < pBlue->GetNumPlayers() ) + { + iTeam = TF_TEAM_RED; + } + else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT || pRed->GetRole() == TEAM_ROLE_DEFENDERS ) + { + // AutoTeam should give new players to the attackers on A/D maps if the teams are even + iTeam = TF_TEAM_BLUE; + } + else + { + if ( nPreferedTeam == TF_TEAM_AUTOASSIGN ) + { + iTeam = RandomInt( 0, 1 ) ? TF_TEAM_RED : TF_TEAM_BLUE; + } + else + { + Assert( nPreferedTeam >= FIRST_GAME_TEAM ); + iTeam = nPreferedTeam; + } + } + } + + return iTeam; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::ShouldForceAutoTeam( void ) +{ + if ( mp_forceautoteam.GetBool() ) + return true; + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + return true; + + if ( TFGameRules() && TFGameRules()->IsCompetitiveMode() ) + return true; + + bool bForce = false; + + // On official servers, and in normal game modes, see if we should re-assign returning players + if ( TFGameRules() && TFGameRules()->IsDefaultGameMode() ) + { + int nTimeSinceLast = TFGameRules()->PlayerHistory_GetTimeSinceLastSeen( this ); + bForce = ( tf_mm_trusted.GetBool() && nTimeSinceLast > 0 && nTimeSinceLast < 60 ); + } + + return bForce; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::HandleCommand_JoinTeam( const char *pTeamName ) +{ + if ( TFGameRules()->State_Get() == GR_STATE_GAME_OVER ) + return; + + if ( GetTeamNumber() == TF_TEAM_RED || GetTeamNumber() == TF_TEAM_BLUE ) + { + const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( TFGameRules()->GetCurrentMatchGroup() ); + if ( pMatchDesc && !pMatchDesc->m_params.m_bAllowTeamChange ) + { + ClientPrint( this, HUD_PRINTCENTER, "#TF_Ladder_NoTeamChange" ); + return; + } +#ifdef STAGING_ONLY + else if ( TFGameRules()->ArePlayersInHell() ) +#else + else if ( TFGameRules()->ArePlayersInHell() || TFGameRules()->IsPowerupMode() ) +#endif // STAGING_ONLY + { + ClientPrint( this, HUD_PRINTCENTER, "#TF_CantChangeTeamNow" ); + return; + } + } + + bool bAutoTeamed = false; + bool bArenaSpectator = false; + + int iTeam = TF_TEAM_RED; + + if ( stricmp( pTeamName, "auto" ) == 0 ) + { + iTeam = GetAutoTeam(); + bAutoTeamed = true; + } + else if ( stricmp( pTeamName, "spectate" ) == 0 ) + { + iTeam = TEAM_SPECTATOR; + } + else if ( stricmp( pTeamName, "spectatearena" ) == 0 ) + { + iTeam = TEAM_SPECTATOR; + + if ( mp_allowspectators.GetBool() == true ) + { + bArenaSpectator = true; + } + } + else + { + for ( int i = 0; i < TF_TEAM_COUNT; ++i ) + { + COMPILE_TIME_ASSERT( TF_TEAM_COUNT == ARRAYSIZE( g_aTeamNames ) ); + if ( stricmp( pTeamName, g_aTeamNames[i] ) == 0 ) + { + iTeam = i; + break; + } + } + } + + // now check if we're limited in our team selection (unless we want to be on the spectator team) + if ( !IsBot() && iTeam != TEAM_SPECTATOR ) + { + int iHumanTeam = TFGameRules()->GetAssignedHumanTeam(); + if ( iHumanTeam != TEAM_ANY ) + { + iTeam = iHumanTeam; + bAutoTeamed = true; + } + } + + // invalid team selection + if ( iTeam < TEAM_SPECTATOR ) + { + return; + } + + if ( IsCoaching() && ( iTeam != TEAM_SPECTATOR ) ) + return; + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() ) + { + if ( !IsBot() && iTeam != TEAM_SPECTATOR ) + { + // human raiders can only be on the blue team + CTeam *raidingTeam = GetGlobalTeam( TF_TEAM_BLUE ); + int humanCount = 0; + for( int i=0; i<raidingTeam->GetNumPlayers(); ++i ) + { + if ( raidingTeam->GetPlayer(i)->IsBot() ) + continue; + + ++humanCount; + } + + if ( humanCount < tf_raid_team_size.GetInt() ) + { + iTeam = TF_TEAM_BLUE; + } + else + { + // no room + iTeam = TEAM_SPECTATOR; + } + } + } + + if ( TFGameRules()->IsBossBattleMode() ) + { + if ( !IsBot() && iTeam != TEAM_SPECTATOR ) + { + // players can only be on the blue team + if ( GetGlobalTeam( TF_TEAM_BLUE )->GetNumPlayers() < tf_boss_battle_team_size.GetInt() ) + { + iTeam = TF_TEAM_BLUE; + } + else + { + // no room + iTeam = TEAM_SPECTATOR; + } + } + + DuelMiniGame_NotifyPlayerChangedTeam( this, iTeam, true ); + ChangeTeam( iTeam, true ); + + return; + } +#endif // TF_RAID_MODE + + // Some game modes will overrule our player-based logic + iTeam = TFGameRules()->GetTeamAssignmentOverride( this, iTeam ); + + if ( iTeam == TEAM_SPECTATOR || ( TFGameRules()->IsInArenaMode() && tf_arena_use_queue.GetBool() && GetTeamNumber() <= LAST_SHARED_TEAM ) ) + { + // Prevent this is the cvar is set + if ( ( mp_allowspectators.GetBool() == false ) && !IsHLTV() && !IsReplay() && TFGameRules()->IsInArenaMode() == false ) + { + ClientPrint( this, HUD_PRINTCENTER, "#Cannot_Be_Spectator" ); + return; + } + + // Deny spectator access if it would unbalance the teams + if ( ( mp_spectators_restricted.GetBool() || tf_mm_trusted.GetBool() ) && TFGameRules() && !TFGameRules()->IsMannVsMachineMode() ) + { + if ( GetTeamNumber() == TF_TEAM_RED || GetTeamNumber() == TF_TEAM_BLUE ) + { + CTeam *pRedTeam = GetGlobalTeam( TF_TEAM_RED ); + CTeam *pBlueTeam = GetGlobalTeam( TF_TEAM_BLUE ); + if ( pRedTeam && pBlueTeam ) + { + int nRedCount = pRedTeam->GetNumPlayers(); + int nBlueCount = pBlueTeam->GetNumPlayers(); + int nGap = GetTeamNumber() == TF_TEAM_RED ? ( nBlueCount - nRedCount ) : ( nRedCount - nBlueCount ); + if ( nGap >= mp_teams_unbalance_limit.GetInt() ) + { + ClientPrint( this, HUD_PRINTCENTER, "#Cannot_Be_Spectator_Unbalance" ); + return; + } + } + } + } + + if ( GetTeamNumber() != TEAM_UNASSIGNED && !IsDead() ) + { + CommitSuicide( false, true ); + } + + m_bArenaSpectator = bArenaSpectator; + DuelMiniGame_NotifyPlayerChangedTeam( this, TEAM_SPECTATOR, true ); + ChangeTeam( TEAM_SPECTATOR ); + + if ( m_bArenaSpectator == true ) + { + SetDesiredPlayerClassIndex( TF_CLASS_UNDEFINED ); + TFGameRules()->Arena_ClientDisconnect( GetPlayerName() ); + TFGameRules()->RemovePlayerFromQueue( this ); + } + + // 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 ); + } + + if ( TFGameRules()->IsInArenaMode() == true && m_bArenaSpectator == false ) + { + ShowViewPortPanel( PANEL_CLASS_BLUE ); + } + } + else + { + if ( iTeam == GetTeamNumber() ) + { + return; // we wouldn't change the team + } + + if ( TFGameRules() && TFGameRules()->IsInHighlanderMode() ) + { + CTFTeam *pTeam = TFTeamMgr()->GetTeam( iTeam ); + if ( pTeam ) + { + if ( pTeam->GetNumPlayers() >= TF_LAST_NORMAL_CLASS - 1 ) + { + // if this join would put too many players on the team in Highlander mode, refuse + // come up with a better way to tell the player they tried to join a full team! + ShowViewPortPanel( PANEL_TEAM ); + return; + } + } + } + + // 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; + } + + DuelMiniGame_NotifyPlayerChangedTeam( this, iTeam, true ); + bool bSilent = TFGameRules() && TFGameRules()->IsPVEModeActive() && IsBot(); + +#ifndef _DEBUG + TFGameRules()->SetPlayerReadyState( entindex(), false ); + TFGameRules()->SetTeamReadyState( false, GetTeamNumber() ); +#endif // _DEBUG + + ChangeTeam( iTeam, bAutoTeamed, bSilent ); + + if ( tf_arena_force_class.GetBool() == false ) + { + 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 ) + { + COMPILE_TIME_ASSERT( TF_TEAM_COUNT == ARRAYSIZE( g_aTeamNames ) ); + 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, bool bFullTeamSwitch ) +{ + int iNewTeam = iTeamNum; + + if ( iNewTeam == TF_TEAM_AUTOASSIGN ) + { + iNewTeam = GetAutoTeam(); + } + + if ( !GetGlobalTeam( iNewTeam ) ) + { + Warning( "CTFPlayer::ForceChangeTeam( %d ) - invalid team index.\n", iNewTeam ); + return; + } + + // Some game modes will overrule our player-based logic + iNewTeam = TFGameRules()->GetTeamAssignmentOverride( this, iNewTeam ); + + int iOldTeam = GetTeamNumber(); + + // if this is our current team, just abort + if ( iNewTeam == iOldTeam ) + return; + + // can't change teams if in a duel + if ( DuelMiniGame_IsInDuel( this ) ) + { + if ( !m_bIsCoaching ) + return; + + DuelMiniGame_NotifyPlayerChangedTeam( this, iTeamNum, true ); + } + + // can't change teams if coaching + if ( m_bIsCoaching && m_hStudent != NULL && iTeamNum != TEAM_SPECTATOR ) + return; + + RemoveAllOwnedEntitiesFromWorld( true ); + + m_iPreviousteam = iOldTeam; + + BaseClass::ChangeTeam( iNewTeam, false, true ); + + if ( !bFullTeamSwitch ) + { + RemoveNemesisRelationships(); + + if ( TFGameRules() && TFGameRules()->IsInHighlanderMode() ) + { + if ( IsAlive() ) + { + CommitSuicide( false, true ); + } + + ResetPlayerClass(); + } + } + + if ( iNewTeam == TEAM_UNASSIGNED ) + { + StateTransition( TF_STATE_OBSERVER ); + } + else if ( iNewTeam == TEAM_SPECTATOR ) + { + StateTransition( TF_STATE_OBSERVER ); + + RemoveAllWeapons(); + DestroyViewModels(); + + if ( TFGameRules()->IsInArenaMode() == true && tf_arena_use_queue.GetBool() == true ) + { + TFGameRules()->AddPlayerToQueueHead( this ); + } + } + + DropFlag(); + + // Don't modify living players in any way +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::HandleFadeToBlack( void ) +{ + if ( mp_fadetoblack.GetBool() ) + { + SetObserverMode( OBS_MODE_CHASE ); + + color32_s clr = { 0,0,0,255 }; + UTIL_ScreenFade( this, clr, 0.75, 0, FFADE_OUT | FFADE_STAYOUT ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ChangeTeam( int iTeamNum, bool bAutoTeam, bool bSilent, bool bAutoBalance /*= false*/ ) +{ + if ( !GetGlobalTeam( iTeamNum ) ) + { + Warning( "CTFPlayer::ChangeTeam( %d ) - invalid team index.\n", iTeamNum ); + return; + } + + // game rules don't allow to change team + if ( TFGameRules() && !TFGameRules()->CanChangeTeam( GetTeamNumber() ) ) + { + return; + } + + // Not allowed to change teams when a ghost + if ( m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) ) + { + return; + } + + // Not allowed to change teams in bumper kart + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + return; + } + + // can only be on TEAM_SPECTATOR when coaching + if ( IsCoaching() && ( iTeamNum >= FIRST_GAME_TEAM ) ) + { + return; + } + + // Some game modes will overrule our player-based logic + iTeamNum = TFGameRules()->GetTeamAssignmentOverride( this, iTeamNum, bAutoBalance ); + + int iOldTeam = GetTeamNumber(); + + // if this is our current team, just abort + if ( iTeamNum == iOldTeam ) + return; + + RemoveAllOwnedEntitiesFromWorld( true ); + + bool bNoTeam = GetTeamNumber() == TEAM_UNASSIGNED; + + m_iPreviousteam = iOldTeam; + + CTF_GameStats.Event_TeamChange( this, iOldTeam, iTeamNum ); + + m_iTeamChanges++; + + // If joining the underdog team, make next spawn instant (autobalance, paladins) + if ( TFGameRules() && TFGameRules()->IsDefaultGameMode() && GetTeamNumber() >= FIRST_GAME_TEAM ) + { + int nStackedTeam, nWeakTeam; + if ( TFGameRules()->AreTeamsUnbalanced( nStackedTeam, nWeakTeam ) ) + { + if ( iTeamNum == nWeakTeam ) + { + AllowInstantSpawn(); + } + } + } + + BaseClass::ChangeTeam( iTeamNum, bAutoTeam, bSilent, bAutoBalance ); + + if ( TFGameRules() && TFGameRules()->IsInHighlanderMode() ) + { + if ( IsAlive() ) + { + CommitSuicide( false, true ); + } + + ResetPlayerClass(); + } + + RemoveNemesisRelationships(); + + if ( iTeamNum == TEAM_UNASSIGNED ) + { + StateTransition( TF_STATE_OBSERVER ); + } + else if ( iTeamNum == TEAM_SPECTATOR ) + { + StateTransition( TF_STATE_OBSERVER ); + + RemoveAllWeapons(); + DestroyViewModels(); + + if ( TFGameRules()->IsInArenaMode() == true && bNoTeam == false && tf_arena_use_queue.GetBool() == true ) + { + TFGameRules()->AddPlayerToQueue( this ); + } + } + else // active player + { + bool bKill = true; + +#ifdef STAGING_ONLY + bKill = ( m_Shared.InCond( TF_COND_REPROGRAMMED ) ) ? false : true; +#endif // STAGING_ONLY + + if ( bKill && !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 ) + { + 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(); + } + } + } + } + + m_Shared.RemoveAllCond(); + DuelMiniGame_NotifyPlayerChangedTeam( this, iTeamNum, false ); + +#ifdef STAGING_ONLY + if ( TFGameRules() && TFGameRules()->GameModeUsesExperience() ) + { + RefundExperiencePoints(); + } +#endif // STAGING_ONLY +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ResetPlayerClass( void ) +{ + if ( GetPlayerClass() ) + { + GetPlayerClass()->Reset(); + } + + SetDesiredPlayerClassIndex( TF_CLASS_UNDEFINED ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::HandleCommand_JoinClass( const char *pClassName, bool bAllowSpawn /* = true */ ) +{ + VPROF_BUDGET( "CTFPlayer::HandleCommand_JoinClass", VPROF_BUDGETGROUP_PLAYER ); + if ( TFGameRules()->State_Get() == GR_STATE_GAME_OVER ) + { + return; + } + +// if ( TFGameRules()->ArePlayersInHell() && ( m_Shared.m_iDesiredPlayerClass > TF_CLASS_UNDEFINED ) ) +// { +// ClientPrint( this, HUD_PRINTCENTER, "#TF_CantChangeClassNow" ); +// return; +// } + + if ( TFGameRules()->IsCompetitiveMode() ) + { + if ( !tf_tournament_classchange_allowed.GetBool() && + TFGameRules()->State_Get() == GR_STATE_RND_RUNNING ) + { + ClientPrint( this, HUD_PRINTCENTER, "#TF_Ladder_NoClassChangeRound" ); + return; + } + + if ( !tf_tournament_classchange_ready_allowed.GetBool() && + TFGameRules()->State_Get() == GR_STATE_BETWEEN_RNDS && + TFGameRules()->IsPlayerReady( entindex() ) ) + { + ClientPrint( this, HUD_PRINTCENTER, "#TF_Ladder_NoClassChangeReady" ); + return; + } + } + + if ( IsCoaching() ) + return; + + // can only join a class after you join a valid team + if ( GetTeamNumber() <= LAST_SHARED_TEAM && TFGameRules()->IsInArenaMode() == false ) + 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() && TFGameRules()->IsInArenaMode() == false ) + { + if ( IsAlive() && !TFGameRules()->CanChangeClassInStalemate() ) + { + char szTime[6]; + Q_snprintf( szTime, sizeof( szTime ), "%d", tf_stalematechangeclasstime.GetInt() ); + + ClientPrint( this, HUD_PRINTTALK, "#game_stalemate_cant_change_class", szTime ); + return; + } + } + + if ( TFGameRules()->IsInArenaMode() == true && IsAlive() == true ) + { + if ( GetTeamNumber() > LAST_SHARED_TEAM && TFGameRules()->InStalemate() == true ) + { + ClientPrint( this, HUD_PRINTTALK, "#TF_Arena_NoClassChange" ); + return; + } + } + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() && GetTeamNumber() == TF_TEAM_BLUE && !tf_raid_allow_class_change.GetBool() ) + { + CTFNavArea *area = (CTFNavArea *)GetLastKnownArea(); + + if ( area && !area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) + { + ClientPrint( this, HUD_PRINTTALK, "No class changes after leaving the safe room" ); + return; + } + } +#endif // TF_RAID_MODE + + if ( TFGameRules()->IsMannVsMachineMode() && GetTeamNumber() == TF_TEAM_PVE_DEFENDERS ) + { + if ( m_nCanPurchaseUpgradesCount > 0 ) + { + ClientPrint( this, HUD_PRINTCENTER, "#TF_MVM_NoClassUpgradeUI" ); + return; + } + + if ( IsReadyToPlay() && !TFGameRules()->InSetup() && g_pPopulationManager && !g_pPopulationManager->IsInEndlessWaves() ) + { + ClientPrint( this, HUD_PRINTTALK, "#TF_MVM_NoClassChangeAfterSetup" ); + return; + } + } + + int iClass = TF_CLASS_UNDEFINED; + bool bShouldNotRespawn = false; + + if ( !bAllowSpawn || ( ( TFGameRules()->State_Get() == GR_STATE_TEAM_WIN ) && ( TFGameRules()->GetWinningTeam() != GetTeamNumber() ) ) ) + { + m_bAllowInstantSpawn = false; + bShouldNotRespawn = true; + } + + if ( stricmp( pClassName, "random" ) != 0 && stricmp( pClassName, "auto" ) != 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; + } + } + + bool bCivilianOkay = false; + + if ( !bCivilianOkay && ( i >= TF_LAST_NORMAL_CLASS ) ) + { + Warning( "HandleCommand_JoinClass( %s ) - invalid class name.\n", pClassName ); + return; + } + + // Check class limits + if ( !TFGameRules()->CanPlayerChooseClass(this, iClass) ) + { + ShowViewPortPanel( ( GetTeamNumber() == TF_TEAM_RED ) ? PANEL_CLASS_RED : PANEL_CLASS_BLUE ); + return; + } + } + else + { + int iTries = 20; + // 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 - 1 ); // -1 to remove the civilian from the randomness + iTries--; + } while( iClass == GetPlayerClass()->GetClassIndex() || (iTries > 0 && !TFGameRules()->CanPlayerChooseClass(this,iClass)) ); + + if ( iTries <= 0 ) + { + // We failed to find a random class. Bring up the class menu again. + ShowViewPortPanel( ( GetTeamNumber() == TF_TEAM_RED ) ? PANEL_CLASS_RED : PANEL_CLASS_BLUE ); + return; + } + } + + if ( TFGameRules() && TFGameRules()->State_Get() == GR_STATE_RND_RUNNING ) + { + // Bit field of classes played during the game + CSteamID steamID; + GetSteamID( &steamID ); + + CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch(); + if ( pMatch ) + { + CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( steamID ); + if ( pMatchPlayer ) + { + pMatchPlayer->UpdateClassesPlayed( GetPlayerClass()->GetClassIndex() ); + } + } + } + +#if defined( _DEBUG ) || defined( STAGING_ONLY ) + if ( mp_developer.GetBool() && !IsBot() ) + { + Vector vPos = GetAbsOrigin(); + QAngle qAngle = GetAbsAngles(); + SetDesiredPlayerClassIndex( iClass ); + ForceRespawn(); + Teleport( &vPos, &qAngle, &vec3_origin ); + return; + } +#endif // _DEBUG || STAGING_ONLY + + // 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; + } + + if ( TFGameRules()->IsInArenaMode() && tf_arena_use_queue.GetBool() == true && GetTeamNumber() <= LAST_SHARED_TEAM ) + { + TFGameRules()->AddPlayerToQueue( this ); + } + + // @note Tom Bui: we need to restrict the UI somehow + // if there's a class restriction on duels... + int iDuelClass = DuelMiniGame_GetRequiredPlayerClass( this ); + if ( iDuelClass >= TF_FIRST_NORMAL_CLASS && iDuelClass < TF_LAST_NORMAL_CLASS ) + { + iClass = iDuelClass; + } + + 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() && TFGameRules()->IsInWaitingForPlayers() == false ) + { + // Stalemate overrides respawn rules. Only allow spawning if we're in the class change time. + bInStalemateClassChangeTime = TFGameRules()->CanChangeClassInStalemate(); + bDeadInstantSpawn = false; + bInRespawnRoom = false; + } + + if ( TFGameRules()->IsInArenaMode() == true ) + { + if ( TFGameRules()->IsInWaitingForPlayers() == false ) + { + bDeadInstantSpawn = false; + + if ( TFGameRules()->InStalemate() == false && TFGameRules()->State_Get() != GR_STATE_TEAM_WIN ) + { + bInRespawnRoom = true; + bShouldNotRespawn = false; + } + else + { + bShouldNotRespawn = true; + + if ( tf_arena_use_queue.GetBool() == false ) + return; + } + } + else if ( tf_arena_use_queue.GetBool() == false ) + { + return; + } + } + + if ( TFGameRules()->IsMannVsMachineMode() && TFGameRules()->State_Get() == GR_STATE_BETWEEN_RNDS ) + m_bAllowInstantSpawn = true; + + if ( bShouldNotRespawn == false && ( m_bAllowInstantSpawn || bDeadInstantSpawn || bInRespawnRoom || bInStalemateClassChangeTime ) ) + { + ForceRespawn(); + +#ifdef STAGING_ONLY + if ( TFGameRules() && TFGameRules()->GameModeUsesExperience() ) + { + RefundExperiencePoints(); + } +#endif // STAGING_ONLY + + 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 ); + } + +#ifdef STAGING_ONLY + if ( TFGameRules() && TFGameRules()->GameModeUsesExperience() ) + { + SetExperiencePoints( 0 ); + SetCurrency( 0 ); + SetExperienceLevel( 1 ); + } +#endif // STAGING_ONLY +} + +//----------------------------------------------------------------------------- +// Purpose: The GC has told us this player wants to respawn now that their loadout has changed. +//----------------------------------------------------------------------------- +void CTFPlayer::CheckInstantLoadoutRespawn( void ) +{ + // Must be alive + if ( !IsAlive() ) + return; + + // In a respawn room + if ( !PointInRespawnRoom( this, WorldSpaceCenter() ) ) + return; + + // Not in stalemate (beyond the change class period) + if ( TFGameRules()->InStalemate() && !TFGameRules()->CanChangeClassInStalemate() ) + return; + + // Not in Arena mode + if ( TFGameRules()->IsInArenaMode() == true ) + return; + + // Not if we're on the losing team + if ( TFGameRules()->State_Get() == GR_STATE_TEAM_WIN && TFGameRules()->GetWinningTeam() != GetTeamNumber() ) + return; + + // Not if our current class's loadout hasn't changed + int iClass = GetPlayerClass() ? GetPlayerClass()->GetClassIndex() : TF_CLASS_UNDEFINED; + if ( iClass >= TF_FIRST_NORMAL_CLASS && iClass < TF_LAST_NORMAL_CLASS ) + { + if ( m_Inventory.ClassLoadoutHasChanged( iClass ) ) + { + if ( m_Shared.InCond( TF_COND_AIMING ) ) + { + // If we are in condition TF_COND_AIMING it will be removed during the ForceRespawn() so we need to reset the weapon + // (which is normally skipped while regenerating)...this only affects the Minigun and the Sniper Rifle. + CTFWeaponBase *pWeapon = GetActiveTFWeapon(); + if ( pWeapon ) + { + if ( IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) || WeaponID_IsSniperRifle( pWeapon->GetWeaponID() ) ) + { + pWeapon->WeaponReset(); + } + } + } + + if ( IsPlayerClass( TF_CLASS_MEDIC ) ) + { + CWeaponMedigun *pMedigun = dynamic_cast< CWeaponMedigun* >( GetActiveTFWeapon() ); + if ( pMedigun ) + { + pMedigun->Lower(); + } + } + + // We want to use ForceRespawn() here so the player is physically moved back + // into the spawn room and not just regenerated instantly in the doorway + ForceRegenerateAndRespawn(); + } + } +} + +class CGC_RespawnPostLoadoutChange : public GCSDK::CGCClientJob +{ +public: + CGC_RespawnPostLoadoutChange( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CGCMsg<MsgGCRespawnPostLoadoutChange_t> msg( pNetPacket ); + CSteamID steamID = msg.Body().m_ulInitiatorSteamID; + + // Find the player with this steamID + CSteamID tmpID; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pPlayer ) + continue; + if ( !pPlayer->GetSteamID( &tmpID ) ) + continue; + + if ( tmpID == steamID ) + { + pPlayer->CheckInstantLoadoutRespawn(); + break; + } + } + + return true; + } +}; +GC_REG_JOB( GCSDK::CGCClient, CGC_RespawnPostLoadoutChange, "CGC_RespawnPostLoadoutChange", k_EMsgGCRespawnPostLoadoutChange, GCSDK::k_EServerTypeGCClient ); + +#if defined (_DEBUG) +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static void DebugEconItemView( const char *pszDescStr, CEconItemView *pEconItemView ) +{ + if ( !pEconItemView ) + return; + + const GameItemDefinition_t *pItemDef = pEconItemView->GetItemDefinition(); + Assert( pItemDef ); + + Warning("%s: \"%s\"\n", pszDescStr, pItemDef->GetDefinitionName() ); +} +#endif + +bool CTFPlayer::ClientCommand( const CCommand &args ) +{ + const char *pcmd = args[0]; + + m_flLastAction = gpGlobals->curtime; + + if ( FStrEq( pcmd, "addcond" ) ) + { + if ( sv_cheats->GetBool() && args.ArgC() >= 2 ) + { + ETFCond eCond = (ETFCond)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( eCond, flDuration ); + } + else + { + pTargetPlayer->m_Shared.AddCond( eCond ); + } + } + return true; + } + else if ( FStrEq( pcmd, "removecond" ) ) + { + if ( sv_cheats->GetBool() && args.ArgC() >= 2 ) + { + CTFPlayer *pTargetPlayer = this; + if ( args.ArgC() >= 3 ) + { + // 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[2] ) ) + { + pTargetPlayer = ToTFPlayer(pPlayer); + break; + } + } + } + } + + ETFCond eCond = (ETFCond)clamp( atoi( args[1] ), 0, TF_COND_LAST-1 ); + pTargetPlayer->m_Shared.RemoveCond( eCond ); + } + return true; + } +#ifdef _DEBUG + else if ( FStrEq( pcmd, "burn" ) ) + { + m_Shared.Burn( this, GetActiveTFWeapon() ); + return true; + } + else if ( FStrEq( pcmd, "bleed" ) ) + { + m_Shared.MakeBleed( this, GetActiveTFWeapon(), 10.0f ); + return true; + } + else if ( FStrEq( pcmd, "dump_damagers" ) ) + { + m_AchievementData.DumpDamagers(); + return true; + } + else if ( FStrEq( pcmd, "stun" ) ) + { + if ( args.ArgC() >= 4 ) + { + m_Shared.StunPlayer( atof(args[1]), atof(args[2]), atof(args[3]) ); + } + return true; + } +// else if ( FStrEq( pcmd, "decoy" ) ) +// { +// CBotNPCDecoy *decoy = (CBotNPCDecoy *)CreateEntityByName( "bot_npc_decoy" ); +// if ( decoy ) +// { +// decoy->SetOwnerEntity( this ); +// DispatchSpawn( decoy ); +// } +// return true; +// } + else if ( FStrEq( pcmd, "tada" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + Taunt( TAUNT_SHOW_ITEM ); + } + return true; + } +// else if ( FStrEq( pcmd, "player_disguise" ) ) +// { +// CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByName( args[1] ) ); +// pPlayer->m_Shared.Disguise( Q_atoi( args[2] ), Q_atoi( args[3] ) ); +// return true; +// } + else +#endif + + if ( FStrEq( pcmd, "jointeam" ) ) + { + // don't let them spam the server with changes + if ( GetNextChangeTeamTime() > gpGlobals->curtime ) + return true; + + SetNextChangeTeamTime( gpGlobals->curtime + 2.0f ); // limit to one change every 2 secs + + 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 ( ShouldRunRateLimitedCommand( args ) ) + { + if ( GetTeamNumber() == TEAM_UNASSIGNED ) + { + if ( ShouldForceAutoTeam() ) + { + ChangeTeam( GetAutoTeam(), true, false ); + ShowViewPortPanel( ( GetTeamNumber() == TF_TEAM_BLUE ) ? PANEL_CLASS_BLUE : PANEL_CLASS_RED ); + } + else + { + ShowViewPortPanel( PANEL_TEAM, true ); + } + } + else if ( IsPlayerClass( TF_CLASS_UNDEFINED ) ) + { + if ( tf_arena_force_class.GetBool() == false ) + { + 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" ) ) + { + // don't let them spam the server with changes + if ( GetNextChangeClassTime() > gpGlobals->curtime ) + return true; + + SetNextChangeClassTime( gpGlobals->curtime + 0.5 ); // limit to one change every 0.5 secs + + if ( tf_arena_force_class.GetBool() == false ) + { + if ( args.ArgC() >= 2 ) + { + HandleCommand_JoinClass( args[1] ); + } + } + return true; + } + else if ( FStrEq( pcmd, "resetclass" ) ) + { + if ( TFGameRules() && TFGameRules()->IsInHighlanderMode() && ( GetTeamNumber() > LAST_SHARED_TEAM ) ) + { + if ( IsAlive() ) + { + CommitSuicide( false, true ); + } + + ResetPlayerClass(); + ShowViewPortPanel( ( GetTeamNumber() == TF_TEAM_RED ) ? PANEL_CLASS_RED : PANEL_CLASS_BLUE ); + } + + return true; + } + else if ( FStrEq( pcmd, "mp_playgesture" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + 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 ( ShouldRunRateLimitedCommand( args ) ) + { + 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" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + // player clicked on the PDA, play attack animation + CTFWeaponBase *pWpn = GetActiveTFWeapon(); + CTFWeaponPDA *pPDA = dynamic_cast<CTFWeaponPDA *>( pWpn ); + + if ( pPDA && !m_Shared.InCond( TF_COND_DISGUISED ) ) + { + DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRIMARY ); + } + } + return true; + } + else if ( FStrEq( pcmd, "weapon_taunt" ) || FStrEq( pcmd, "taunt" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + int iTauntSlot = args.ArgC() == 2 ? atoi( args[1] ) : 0; + HandleTauntCommand( iTauntSlot ); + } + return true; + } + else if ( FStrEq( pcmd, "stop_taunt" ) ) + { + if( m_Shared.GetTauntIndex() == TAUNT_LONG && !m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + EndLongTaunt(); + } + + return true; + } + else if ( FStrEq( pcmd, "-taunt" ) ) + { + // DO NOTHING + // We changed taunt key to be press to toggle instead of press and hold to do long taunt + return true; + } + else if ( FStrEq( pcmd, "td_buyback" ) ) + { + if ( TFGameRules() && TFGameRules()->IsPVEModeActive() && IsObserver() && ( GetTeamNumber() > TEAM_SPECTATOR ) ) + { + // Make sure we're not still in freezecam + int iObsMode = GetObserverMode(); + if ( iObsMode == OBS_MODE_FREEZECAM || iObsMode == OBS_MODE_DEATHCAM ) + return true; + + float flWaveTime = TFGameRules()->GetNextRespawnWave( GetTeamNumber(), this ); + + bool bSuccess = false; + int iRespawnWait = (flWaveTime - gpGlobals->curtime); + int iCost = iRespawnWait * MVM_BUYBACK_COST_PER_SEC; + + // New system (finite buybacks per-wave, not currency-based) + if ( tf_mvm_buybacks_method.GetBool() ) + { + if ( g_pPopulationManager->GetNumBuybackCreditsForPlayer( this ) ) + { + bSuccess = true; + iCost = 1; + g_pPopulationManager->RemoveBuybackCreditFromPlayer( this ); + } + } + // Old system (currency-based) + else + { + if ( GetCurrency() >= iCost ) + { + bSuccess = true; + RemoveCurrency( iCost ); + MannVsMachineStats_PlayerEvent_BoughtInstantRespawn( this, iCost ); + } + } + + if ( bSuccess ) + { + ForceRespawn(); + IGameEvent *event = gameeventmanager->CreateEvent( "player_buyback" ); + if ( event ) + { + event->SetInt( "player", entindex() ); + event->SetInt( "cost", iCost ); + gameeventmanager->FireEvent( event ); + } + } + else + { + CSingleUserRecipientFilter filter( this ); + EmitSound_t params; + params.m_pSoundName = "Player.DenyWeaponSelection"; + EmitSound( filter, entindex(), params ); + } + } + return true; + } + else if ( FStrEq( pcmd, "build" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + if ( TFGameRules()->InStalemate() && mp_stalemate_meleeonly.GetBool() ) + return true; + + // can't issue a build command while carrying an object + if ( m_Shared.IsCarryingObject() ) + return true; + + if ( IsTaunting() ) + return true; + + int iBuilding = 0; + int iMode = 0; + bool bArgsChecked = false; + + // Fixup old binds. + if ( args.ArgC() == 2 ) + { + iBuilding = atoi( args[ 1 ] ); + if ( iBuilding == 3 ) // Teleport exit is now a mode. + { + iBuilding = 1; + iMode = 1; + } + bArgsChecked = true; + } + else if ( args.ArgC() == 3 ) + { + iBuilding = atoi( args[ 1 ] ); + iMode = atoi( args[ 2 ] ); + bArgsChecked = true; + } + + if ( bArgsChecked ) + { + StartBuildingObjectOfType( iBuilding, iMode ); + } + else + { + Warning( "Usage: build <building> <mode>\n" ); + } + } + return true; + } + else if ( FStrEq( pcmd, "destroy" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + if ( IsPlayerClass( TF_CLASS_ENGINEER ) ) // Spies can't destroy buildings (sappers) + { + int iBuilding = 0; + int iMode = 0; + bool bArgsChecked = false; + + // Fixup old binds. + if ( args.ArgC() == 2 ) + { + iBuilding = atoi( args[ 1 ] ); + if ( iBuilding == 3 ) // Teleport exit is now a mode. + { + iBuilding = 1; + iMode = 1; + } + bArgsChecked = true; + } + else if ( args.ArgC() == 3 ) + { + iBuilding = atoi( args[ 1 ] ); + iMode = atoi( args[ 2 ] ); + bArgsChecked = true; + } + + if ( bArgsChecked ) + { + DetonateObjectOfType( iBuilding, iMode ); + } + else + { + Warning( "Usage: destroy <building> <mode>\n" ); + } + } + } + return true; + } + else if ( FStrEq( pcmd, "eureka_teleport" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + CTFWeaponBase* pWeapon = GetActiveTFWeapon(); + if ( !pWeapon ) + return true; + + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + return true; + + int iAltFireTeleportToSpawn = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iAltFireTeleportToSpawn, alt_fire_teleport_to_spawn ); + + if ( IsPlayerClass( TF_CLASS_ENGINEER ) && iAltFireTeleportToSpawn ) + { + if ( args.ArgC() == 2 ) + { + m_eEurekaTeleportTarget = (eEurekaTeleportTargets)atoi( args[1] ); + } + else + { + m_eEurekaTeleportTarget = EUREKA_TELEPORT_HOME; + } + + // Do the Eureka Effect teleport taunt + Taunt( TAUNT_SPECIAL, MP_CONCEPT_TAUNT_EUREKA_EFFECT_TELEPORT ); + } + } + } + else if ( FStrEq( pcmd, "arena_changeclass" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + if ( TFGameRules() && TFGameRules()->IsInArenaMode() && ( tf_arena_force_class.GetBool() == true ) ) + { + if ( TFGameRules()->State_Get() == GR_STATE_PREROUND ) + { + if ( m_Shared.GetArenaNumChanges() < tf_arena_change_limit.GetInt() ) + { + CommitSuicide( true, false ); + m_Shared.IncrementArenaNumChanges(); + } + } + } + } + return true; + } + else if ( FStrEq( pcmd, "extendfreeze" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + m_flDeathTime += 2.0f; + } + return true; + } + else if ( FStrEq( pcmd, "show_motd" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + if ( ShouldForceAutoTeam() ) + { + int nPreferedTeam = TF_TEAM_AUTOASSIGN; + PlayerHistoryInfo_t *pPlayerInfo = ( TFGameRules() ) ? TFGameRules()->PlayerHistory_GetPlayerInfo( this ) : NULL; + if ( pPlayerInfo && pPlayerInfo->nTeam >= FIRST_GAME_TEAM ) + { + nPreferedTeam = pPlayerInfo->nTeam; + } + + int iTeam = GetAutoTeam( nPreferedTeam ); + ChangeTeam( iTeam, true, false ); + ShowViewPortPanel( ( iTeam == TF_TEAM_RED ) ? PANEL_CLASS_RED : PANEL_CLASS_BLUE ); + } +#ifdef TF_RAID_MODE + else if ( TFGameRules()->IsBossBattleMode() ) + { + int iTeam = GetAutoTeam(); + ChangeTeam( iTeam, true ); + ShowViewPortPanel( ( iTeam == TF_TEAM_RED ) ? PANEL_CLASS_RED : PANEL_CLASS_BLUE ); + } +#endif + else + { + ShowViewPortPanel( PANEL_TEAM, false ); + } + ShowViewPortPanel( PANEL_ARENA_TEAM, false ); + + char pszWelcome[128]; + Q_snprintf( pszWelcome, sizeof(pszWelcome), "#TF_Welcome" ); + if ( UTIL_GetActiveHolidayString() ) + { + Q_snprintf( pszWelcome, sizeof(pszWelcome), "#TF_Welcome_%s", UTIL_GetActiveHolidayString() ); + } + + KeyValues *data = new KeyValues( "data" ); + data->SetString( "title", pszWelcome ); // info panel title + data->SetString( "type", "1" ); // show userdata from stringtable entry + data->SetString( "msg", "motd" ); // use this stringtable entry + data->SetString( "msg_fallback", "motd_text" ); // use this stringtable entry if the base is HTML, and client has disabled HTML motds + data->SetBool( "unload", sv_motd_unload_on_dismissal.GetBool() ); + + ShowViewPortPanel( PANEL_INFO, true, data ); + + data->deleteThis(); + } + return true; + } + else if ( FStrEq( pcmd, "show_htmlpage" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + if ( args.ArgC() != 2 ) + { + Warning( "Usage: show_htmlpage <url>\n" ); + return true; + } + + KeyValues *data = new KeyValues( "data" ); + data->SetString( "title", "#TF_Welcome" ); // info panel title + data->SetString( "type", "2" ); // show url + data->SetString( "msg", args[1] ); + data->SetString( "msg_fallback", "motd_text" ); // use this stringtable entry if the base is HTML, and client has disabled HTML motds + data->SetInt( "cmd", TEXTWINDOW_CMD_CLOSED_HTMLPAGE ); // exec this command if panel closed + data->SetString( "customsvr", "1" ); + data->SetBool( "unload", false ); + + ShowViewPortPanel( PANEL_INFO, true, data ); + + data->deleteThis(); + } + return true; + } + else if ( FStrEq( pcmd, "closed_htmlpage" ) ) + { + // Does nothing, it's for server plugins to hook. + return true; + } + else if ( FStrEq( pcmd, "condump_on" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + 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 ( ShouldRunRateLimitedCommand( args ) ) + { + 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; + } + } + } + } + else if ( FStrEq( pcmd, "spec_next" ) ) // chase next player + { + if ( m_bIsCoaching ) + { + return true; + } +// if ( !ShouldRunRateLimitedCommand( args ) ) +// return true; + + // intentionally falling through to the bottom so the baseclass version is called + m_bArenaIsAFK = false; + } + else if ( FStrEq( pcmd, "spec_prev" ) ) // chase prev player + { + if ( m_bIsCoaching ) + { + return true; + } +// if ( !ShouldRunRateLimitedCommand( args ) ) +// return true; + + // intentionally falling through to the bottom so the baseclass version is called + m_bArenaIsAFK = false; + } + else if ( FStrEq( pcmd, "spec_mode" ) ) // set obs mode + { +// if ( !ShouldRunRateLimitedCommand( args ) ) +// return true; + + // intentionally falling through to the bottom so the baseclass version is called + m_bArenaIsAFK = false; + } + else if ( FStrEq( pcmd, "showroundinfo" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + // don't let the player open the round info menu until they're a spectator or they're on a regular team and have picked a class + if ( ( GetTeamNumber() == TEAM_SPECTATOR ) || ( ( GetTeamNumber() != TEAM_UNASSIGNED ) && ( GetPlayerClass()->GetClassIndex() != TF_CLASS_UNDEFINED ) ) ) + { + if ( TFGameRules() ) + { + TFGameRules()->ShowRoundInfoPanel( this ); + } + } + } + + return true; + } +#ifdef STAGING_ONLY + else if ( FStrEq( pcmd, "feigndeath") ) + { + m_Shared.SetFeignDeathReady( true ); + } +#endif // STAGING_ONLY + else if ( FStrEq( pcmd, "autoteam" ) ) + { + if ( !IsCoaching() ) + { + int iTeam = GetAutoTeam(); + ChangeTeam( iTeam, true, false ); + + if ( iTeam > LAST_SHARED_TEAM ) + { + ShowViewPortPanel( ( iTeam == TF_TEAM_RED ) ? PANEL_CLASS_RED : PANEL_CLASS_BLUE ); + } + } + + return true; + } + else if ( FStrEq( pcmd, "coach_command" ) ) + { + if ( m_bIsCoaching && m_hStudent && args.ArgC() > 1 ) + { + eCoachCommand command = (eCoachCommand)atoi( args[1] ); + HandleCoachCommand( this, command ); + return true; + } + } + else if ( FStrEq( pcmd, "boo" ) && m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) ) + { + if ( m_booTimer.IsElapsed() ) + { + m_booTimer.Start( 1.f ); + EmitSound( "Halloween.GhostBoo" ); + } + + return true; + } + else if ( FStrEq( pcmd, "loot_response" ) ) + { + // Only allowed to speak these during post-game MvM + if ( !TFGameRules() + || !TFGameRules()->IsMannVsMachineMode() + || !( TFGameRules()->State_Get() == GR_STATE_GAME_OVER ) ) + { + return true; + } + + if ( FStrEq( args[1], "common" ) ) + { + SpeakConceptIfAllowed( MP_CONCEPT_MVM_LOOT_COMMON ); + return true; + } + else if ( FStrEq( args[1], "rare" ) ) + { + SpeakConceptIfAllowed( MP_CONCEPT_MVM_LOOT_RARE ); + return true; + } + else if ( FStrEq( args[1], "ultra_rare" ) ) + { + SpeakConceptIfAllowed( MP_CONCEPT_MVM_LOOT_ULTRARARE ); + return true; + } + } + else if ( FStrEq( pcmd, "done_viewing_loot" ) ) + { + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && g_pPopulationManager ) + { + g_pPopulationManager->PlayerDoneViewingLoot( this ); + } + return true; + } + else if ( FStrEq( pcmd, "spectate" ) ) + { + HandleCommand_JoinTeam( "spectate" ); + return true; + } + else if ( FStrEq( pcmd, "team_ui_setup" ) ) + { + bool bAutoTeam = ShouldForceAutoTeam(); +#ifdef TF_RAID_MODE + bAutoTeam |= TFGameRules()->IsBossBattleMode(); +#endif + + // For autoteam, display the appropriate team's CLASS selection ui + if ( bAutoTeam ) + { + ChangeTeam( GetAutoTeam(), true, false ); + ShowViewPortPanel( ( GetTeamNumber() == TF_TEAM_BLUE ) ? PANEL_CLASS_BLUE : PANEL_CLASS_RED ); + } + // Otherwise, show TEAM selection ui + else + { + ShowViewPortPanel( PANEL_TEAM ); + } + + return true; + } + else if ( FStrEq( "next_map_vote", pcmd ) ) + { + CTFGameRules::EUserNextMapVote eVoteState = (CTFGameRules::EUserNextMapVote)atoi( args[1] ); + switch( eVoteState ) + { + case CTFGameRules::USER_NEXT_MAP_VOTE_MAP_0: + case CTFGameRules::USER_NEXT_MAP_VOTE_MAP_1: + case CTFGameRules::USER_NEXT_MAP_VOTE_MAP_2: + // Valid + break; + default: + // Invalid + Assert( false ); + return true; + } + + // No flip flop! + if ( TFGameRules()->PlayerNextMapVoteState( entindex() ) != CTFGameRules::USER_NEXT_MAP_VOTE_UNDECIDED ) + return true; + + // Needs to do next-map voting + const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( TFGameRules()->GetCurrentMatchGroup() ); + if ( !pMatchDesc || !pMatchDesc->BUsesMapVoteAfterMatchEnds() ) + return true; + + if ( TFGameRules()->State_Get() != GR_STATE_GAME_OVER ) + return true; + + CMatchInfo* pMatch = GTFGCClientSystem()->GetMatch(); + if ( !pMatch ) + return true; + + TFGameRules()->SetPlayerNextMapVote( entindex(), eVoteState ); + DevMsg( "Settings player %d to rematch vote state %d.\n", entindex(), eVoteState ); + + return true; + } +#ifdef STAGING_ONLY + else if ( FStrEq( pcmd, "reload_extra_models" ) ) + { + for ( int i = 0; i < CExtraMapEntity::AutoList().Count(); i++ ) + { + CExtraMapEntity *pEntity = static_cast<CExtraMapEntity*>( CExtraMapEntity::AutoList()[i] ); + UTIL_Remove( pEntity ); + } + + CExtraMapEntity::SpawnExtraModel(); + return true; + } +#endif // STAGING_ONLY + + return BaseClass::ClientCommand( args ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::SetClassMenuOpen( bool bOpen ) +{ + m_bIsClassMenuOpen = bOpen; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::IsClassMenuOpen( void ) +{ + return m_bIsClassMenuOpen; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::MerasmusPlayerBombExplode( bool bExcludeMe /*= true */ ) +{ + float flDamage = 40.0f; + // bomb head damage is 100 only for fighting Merasmus, lower for all other scenarios + if ( TFGameRules() && TFGameRules()->GetActiveBoss() && ( TFGameRules()->GetActiveBoss()->GetBossType() == HALLOWEEN_BOSS_MERASMUS ) ) + { + flDamage = 100.0f; + } + + // explode! + Vector vecExplosion = EyePosition(); + + CPVSFilter filter( vecExplosion ); + TE_TFExplosion( filter, 0.0f, vecExplosion, Vector(0,0,1), NULL, entindex() ); + int iDmgType = DMG_BLAST | DMG_USEDISTANCEMOD; + CTakeDamageInfo info( this, this, NULL, vecExplosion, vecExplosion, flDamage, iDmgType, TF_DMG_CUSTOM_MERASMUS_PLAYER_BOMB, &vecExplosion ); + + CBaseEntity *pIgnoreEnt = NULL; + if ( bExcludeMe ) + { + pIgnoreEnt = this; + } + + CTFRadiusDamageInfo radiusinfo( &info, vecExplosion, 100.f, pIgnoreEnt ); + TFGameRules()->RadiusDamage( radiusinfo ); + + UTIL_ScreenShake( vecExplosion, 15.0f, 5.0f, 2.f, 750.f, SHAKE_START, true ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::DropDeathCallingCard( CTFPlayer* pTFAttacker, CTFPlayer* pTFVictim ) +{ + int iCallingCard = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pTFAttacker, iCallingCard, calling_card_on_kill ); + if ( iCallingCard ) + { + CEffectData data; + + data.m_vOrigin = pTFVictim->GetAbsOrigin(); + data.m_vAngles = pTFVictim->GetAbsAngles(); + data.m_nAttachmentIndex = pTFVictim->entindex(); // Victim + data.m_nHitBox = entindex(); // iShooter + data.m_fFlags = iCallingCard; // Index to the Calling card + + DispatchEffect( "TFDeathCallingCard", data ); + } +} + +//----------------------------------------------------------------------------- +// 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: +//----------------------------------------------------------------------------- +void CTFPlayer::DetonateObjectOfType( int iType, int iMode, bool bIgnoreSapperState ) +{ + CBaseObject *pObj = GetObjectOfType( iType, iMode ); + if( !pObj ) + return; + + if( !bIgnoreSapperState && ( pObj->HasSapper() || pObj->IsPlasmaDisabled() ) ) + return; + + IGameEvent *event = gameeventmanager->CreateEvent( "object_removed" ); + if ( event ) + { + event->SetInt( "userid", GetUserID() ); // user ID of the object owner + event->SetInt( "objecttype", iType ); // type of object removed + event->SetInt( "index", pObj->entindex() ); // index of the object removed + gameeventmanager->FireEvent( event ); + } + + if ( TFGameRules() && TFGameRules()->GetTrainingModeLogic() && IsFakeClient() == false ) + { + TFGameRules()->GetTrainingModeLogic()->OnPlayerDetonateBuilding( this, pObj ); + } + + 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 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTFPlayer::GetObjectBuildSpeedMultiplier( int iObjectType, bool bIsRedeploy ) const +{ + float flBuildRate = 1.f; // need a base value for mult + + switch( iObjectType ) + { + case OBJ_SENTRYGUN: + CALL_ATTRIB_HOOK_FLOAT( flBuildRate, sentry_build_rate_multiplier ); + flBuildRate += bIsRedeploy ? 2.0 : 0.0f; + break; + + case OBJ_TELEPORTER: + CALL_ATTRIB_HOOK_FLOAT( flBuildRate, teleporter_build_rate_multiplier ); + flBuildRate += bIsRedeploy ? 3.0 : 0.0f; + break; + + case OBJ_DISPENSER: + CALL_ATTRIB_HOOK_FLOAT( flBuildRate, teleporter_build_rate_multiplier ); + flBuildRate += bIsRedeploy ? 3.0 : 0.0f; + break; +#ifdef STAGING_ONLY + // STAGING_ENGY + case OBJ_CATAPULT: + flBuildRate += 5.0f; + flBuildRate += bIsRedeploy ? 3.0 : 0.0f; + break; +#endif + } + + return flBuildRate - 1.0f; // sub out the initial 1 so the final result is added +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + if ( m_takedamage != DAMAGE_YES ) + return; + + CTFPlayer *pAttacker = ToTFPlayer( info.GetAttacker() ); + if ( pAttacker ) + { + // Weapons that use uber ammo can transfer that uber into other medics + if ( pAttacker->IsPlayerClass( TF_CLASS_MEDIC ) && IsPlayerClass( TF_CLASS_MEDIC ) ) + { + CTFWeaponBase *pWep = pAttacker->GetActiveTFWeapon(); + if ( pWep ) + { + float flUberTransfer = pWep->UberChargeAmmoPerShot(); + if ( flUberTransfer > 0.0f ) + { + float flTransferPercent = 0.0f; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pWep, flTransferPercent, ubercharge_transfer ); + + if ( flTransferPercent ) + { + flUberTransfer *= ( flTransferPercent * 0.01f ); + + CWeaponMedigun *pMedigun = static_cast< CWeaponMedigun * >( Weapon_OwnsThisID( TF_WEAPON_MEDIGUN ) ); + if ( pMedigun ) + { + pMedigun->AddCharge( flUberTransfer ); + } + } + } + } + } + + // Prevent team damage here so blood doesn't appear + if ( !g_pGameRules->FPlayerCanTakeDamage( this, pAttacker, 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; + bool bIsHeadshot = false; + + if ( info_modified.GetDamageType() & DMG_USE_HITLOCATIONS ) + { + if ( !m_Shared.InCond( TF_COND_INVULNERABLE ) && ptr->hitgroup == HITGROUP_HEAD ) + { + CTFWeaponBase *pWpn = pAttacker->GetActiveTFWeapon(); + bool bCritical = true; + bIsHeadshot = true; + + if ( pWpn && !pWpn->CanFireCriticalShot( true ) ) + { + bCritical = false; + } + + int iBackheadshot = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetInflictor(), iBackheadshot, back_headshot ); + if ( iBackheadshot ) + { + // only allow if hit in the back of the head + Vector entForward; + AngleVectors( EyeAngles(), &entForward ); + + Vector toEnt = GetAbsOrigin() - pAttacker->GetAbsOrigin(); + toEnt.NormalizeInPlace(); + + // did not backshot + //if ( DotProduct( toEnt, entForward ) <= 0.7071f ) // 0.7 os 45 degress from center + if ( DotProduct( toEnt, entForward ) < 0.5f ) // 60 degrees from center (total of 120) + { + bCritical = false; + bIsHeadshot = false; + } + } + + // Check for headshot damage modifiers + float flHeadshotModifier = 1.0f; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER ( pAttacker, flHeadshotModifier, headshot_damage_modify); + info_modified.ScaleDamage(flHeadshotModifier); + + if ( bCritical ) + { + info_modified.AddDamageType( DMG_CRITICAL ); + + int iDecapType = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER ( pAttacker, iDecapType, decapitate_type); + if ( iDecapType > 0 ) + { + info_modified.SetDamageCustom( TF_DMG_CUSTOM_HEADSHOT_DECAPITATION ); + } + else + { + info_modified.SetDamageCustom( TF_DMG_CUSTOM_HEADSHOT ); + } + + // play the critical shot sound to the shooter + if ( pWpn ) + { + pWpn->WeaponSound( BURST ); + } + } + } + } + + if ( !bIsHeadshot && pAttacker ) + { + // Check for bodyshot damage modifiers + CTFWeaponBase *pWpn = pAttacker->GetActiveTFWeapon(); + float flBodyshotModifier = 1.0f; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER ( pWpn, flBodyshotModifier, bodyshot_damage_modify); + info_modified.ScaleDamage( flBodyshotModifier ); + } + + if ( GetTeamNumber() == TF_TEAM_BLUE ) + { + info_modified.SetDamage( info_modified.GetDamage() * tf_damage_multiplier_blue.GetFloat() ); + } + else if ( GetTeamNumber() == TF_TEAM_RED ) + { + info_modified.SetDamage( info_modified.GetDamage() * tf_damage_multiplier_red.GetFloat() ); + } + + if ( m_Shared.InCond( TF_COND_DISGUISED ) ) + { + // no impact effects + } + else if ( m_Shared.IsInvulnerable() ) + { + // 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 ) +{ + if ( m_Shared.InCond( TF_COND_NOHEALINGDAMAGEBUFF ) ) + { + return 0; // No healing while in this state! + } + + int nResult = 0; + + CTFWeaponBase *pWeapon = GetActiveTFWeapon(); + if ( pWeapon ) + { + float flHealingBonus = 1.f; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pWeapon, flHealingBonus, mult_healing_received ); + flHealth *= flHealingBonus; + } + + // Medigun healing and player/class regen use an accumulator, so they've already factored in debuffs. +// if ( m_Shared.InCond( TF_COND_HEALING_DEBUFF ) && !( bitsDamageType & DMG_IGNORE_DEBUFFS ) ) +// { +// flHealth *= 0.75f; +// } + + // 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; + nResult = flHealth; + } + else + { + float flHealthToAdd = flHealth; + float flMaxHealth = 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 ) + { + nResult = 0; + } + else + { + nResult = BaseClass::TakeHealth( flHealthToAdd, bitsDamageType ); + } + } + + return nResult; +} + +//----------------------------------------------------------------------------- +// 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::BumpWeapon( CBaseCombatWeapon *pWeapon ) +{ + CBaseCombatCharacter *pOwner = pWeapon->GetOwner(); + + // Can I have this weapon type? + if ( !IsAllowedToPickupWeapons() ) + return false; + + if ( pOwner || !Weapon_CanUse( pWeapon ) || !g_pGameRules->CanHavePlayerItem( this, pWeapon ) ) + { + UTIL_Remove( pWeapon ); + return false; + } + + // Don't let the player fetch weapons through walls (use MASK_SOLID so that you can't pickup through windows) + if ( !pWeapon->FVisible( this, MASK_SOLID ) ) + return false; + + // ---------------------------------------- + // If I already have it just take the ammo + // ---------------------------------------- + if (Weapon_OwnsThisType( pWeapon->GetClassname(), pWeapon->GetSubType())) + { + UTIL_Remove( pWeapon ); + return true; + } + else + { + // ------------------------- + // Otherwise take the weapon + // ------------------------- + pWeapon->CheckRespawn(); + + pWeapon->AddSolidFlags( FSOLID_NOT_SOLID ); + pWeapon->AddEffects( EF_NODRAW ); + + Weapon_Equip( pWeapon ); + return true; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::DropCurrentWeapon( void ) +{ + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::DropFlag( bool bSilent /* = false */ ) +{ + if ( HasItem() ) + { + CCaptureFlag *pFlag = dynamic_cast<CCaptureFlag*>( GetItem() ); + if ( pFlag ) + { + int nFlagTeamNumber = pFlag->GetTeamNumber(); + pFlag->Drop( this, true, true, !bSilent ); + IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_flag_event" ); + if ( event ) + { + event->SetInt( "player", entindex() ); + event->SetInt( "eventtype", TF_FLAGEVENT_DROPPED ); + event->SetInt( "priority", 8 ); + event->SetInt( "team", nFlagTeamNumber ); + + gameeventmanager->FireEvent( event ); + } + } + } +} +//----------------------------------------------------------------------------- +// Purpose: Players can drop Powerup Runes +//----------------------------------------------------------------------------- +void CTFPlayer::DropRune( bool bApplyForce /* = true */, int nTeam /* = TEAM_ANY */ ) +{ + if ( m_Shared.IsCarryingRune() ) + { + Vector forward; + EyeVectors( &forward ); + + RuneTypes_t nRuneType = m_Shared.GetCarryingRuneType(); + // We expect that we are actually are carrying here, so assert that we are. + Assert( nRuneType >= 0 && nRuneType < RUNE_TYPES_MAX ); + + m_Shared.SetCarryingRuneType( RUNE_NONE ); + + bool bShouldRemoveMeleeOnly = !( IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && m_Shared.InCond( TF_COND_ENERGY_BUFF ) ); + if ( bShouldRemoveMeleeOnly ) + { + m_Shared.RemoveCond( TF_COND_CANNOT_SWITCH_FROM_MELEE ); // Knockout powerup sets this to on + } + TeamFortress_SetSpeed(); // Need to call this or speed bonus isn't removed immediately + CTFRune::CreateRune( GetAbsOrigin(), nRuneType, nTeam, true, bApplyForce, forward ); // Manually dropped powerups are always neutral + } +} + +//----------------------------------------------------------------------------- +// 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; + } + + CUtlVector<int> potentialTargets; + + 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 and attempt to find a player as the team/class we're disguising as + int i; + for ( i = 1; i <= gpGlobals->maxClients; i++ ) + { + pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && ( pPlayer != pLastTarget ) ) + { + // First, try to find a player with the same color AND skin + if ( ( pPlayer->GetTeamNumber() == nTeam ) && ( pPlayer->GetPlayerClass()->GetClassIndex() == nClass ) ) + { + potentialTargets.AddToHead( i ); + } + } + } + + // do we have any potential targets in the list? + if ( potentialTargets.Count() > 0 ) + { + int iIndex = random->RandomInt( 0, potentialTargets.Count() - 1 ); + return UTIL_PlayerByIndex( potentialTargets[iIndex] ); + } + + // we didn't find someone with the class, so just find someone with the same team color + for ( i = 1; i <= gpGlobals->maxClients; i++ ) + { + pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && ( pPlayer->GetTeamNumber() == nTeam ) ) + { + potentialTargets.AddToHead( i ); + } + } + + if ( potentialTargets.Count() > 0 ) + { + int iIndex = random->RandomInt( 0, potentialTargets.Count() - 1 ); + return UTIL_PlayerByIndex( potentialTargets[iIndex] ); + } + + // we didn't find anyone + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +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; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::SetBlastJumpState( int iState, bool bPlaySound /*= false*/ ) +{ + m_iBlastJumpState |= iState; + + const char *pszEvent = NULL; + if ( iState == TF_PLAYER_STICKY_JUMPED ) + { + pszEvent = "sticky_jump"; + } + else if ( iState == TF_PLAYER_ROCKET_JUMPED ) + { + pszEvent = "rocket_jump"; + } + + if ( pszEvent ) + { + IGameEvent * event = gameeventmanager->CreateEvent( pszEvent ); + if ( event ) + { + event->SetInt( "userid", GetUserID() ); + event->SetBool( "playsound", bPlaySound ); + gameeventmanager->FireEvent( event ); + } + } + + m_Shared.AddCond( TF_COND_BLASTJUMPING ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ClearBlastJumpState( void ) +{ + m_bCreatedRocketJumpParticles = false; + m_iBlastJumpState = 0; + m_flBlastJumpLandTime = gpGlobals->curtime; + m_Shared.RemoveCond( TF_COND_BLASTJUMPING ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void HandleRageGain( CTFPlayer *pPlayer, unsigned int iRequiredBuffFlags, float flDamage, float fInverseRageGainScale ) +{ + if ( !pPlayer ) + return; + + if ( pPlayer->IsPlayerClass( TF_CLASS_SOLDIER ) ) + { + CTFBuffItem *pBuffItem = dynamic_cast<CTFBuffItem*>( pPlayer->Weapon_OwnsThisID( TF_WEAPON_BUFF_ITEM ) ); + unsigned int iBuffId = pBuffItem ? pBuffItem->GetBuffType() : 0; + if ( iBuffId < ARRAYSIZE( g_RageBuffTypes ) ) + { + if ( g_RageBuffTypes[iBuffId].m_iBuffFlags & iRequiredBuffFlags ) + { + pPlayer->m_Shared.ModifyRage( g_RageBuffTypes[iBuffId].m_fRageScale * ( flDamage / fInverseRageGainScale ) ); + } + } + } + else if ( pPlayer->IsPlayerClass( TF_CLASS_PYRO ) ) + { + CTFFlameThrower *pFlameThrower = dynamic_cast<CTFFlameThrower*>( pPlayer->Weapon_OwnsThisID( TF_WEAPON_FLAMETHROWER ) ); + unsigned int iBuffId = pFlameThrower ? pFlameThrower->GetBuffType() : 0; + if ( iBuffId < ARRAYSIZE( g_RageBuffTypes ) ) + { + if ( g_RageBuffTypes[iBuffId].m_iBuffFlags & iRequiredBuffFlags ) + { + if ( TFGameRules() && TFGameRules()->IsPowerupMode() && pPlayer->m_Shared.GetCarryingRuneType() != RUNE_NONE ) + { + pPlayer->m_Shared.ModifyRage(g_RageBuffTypes[iBuffId].m_fRageScale * ( ( flDamage / 10 ) / fInverseRageGainScale) ); + } + else + { + pPlayer->m_Shared.ModifyRage( g_RageBuffTypes[iBuffId].m_fRageScale * ( flDamage / fInverseRageGainScale ) ); + } + } + } + } + + // General + int iRage = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pPlayer, iRage, generate_rage_on_dmg ); + if ( iRage ) + { + if ( pPlayer->IsPlayerClass( TF_CLASS_ENGINEER ) && ( kRageBuffFlag_OnDamageDealt & iRequiredBuffFlags ) ) + { + pPlayer->m_Shared.ModifyRage( flDamage ); + } + else if ( pPlayer->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && ( kRageBuffFlag_OnDamageDealt & iRequiredBuffFlags ) ) + { + pPlayer->m_Shared.ModifyRage( 0.22f * ( flDamage / fInverseRageGainScale ) ); + } + } + + int iHealRage = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pPlayer, iHealRage, generate_rage_on_heal ); // ...lol + if ( iHealRage ) + { + if ( pPlayer->IsPlayerClass( TF_CLASS_MEDIC ) && ( kRageBuffFlag_OnHeal & iRequiredBuffFlags ) ) + { + pPlayer->m_Shared.ModifyRage( 0.25f * flDamage ); + } + } + +#ifdef STAGING_ONLY + if ( TFGameRules()->GameModeUsesUpgrades() && pPlayer->IsPlayerClass( TF_CLASS_SPY ) && ( kRageBuffFlag_OnDamageDealt & iRequiredBuffFlags ) ) + { + // Don't allow when radius cloak is in effect + if ( !pPlayer->m_Shared.InCond( TF_COND_STEALTHED_USER_BUFF ) ) + { + pPlayer->m_Shared.ModifyRage( 0.025f * flDamage ); + } + } +#endif // STAGING_ONLY +} + +// we want to ship this...do not remove +ConVar tf_debug_damage( "tf_debug_damage", "0", FCVAR_CHEAT ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTFPlayer::OnTakeDamage( const CTakeDamageInfo &inputInfo ) +{ + CTakeDamageInfo info = inputInfo; + + bool bIsObject = info.GetInflictor() && info.GetInflictor()->IsBaseObject(); + +// need to check this now, before dying + bool bHadBallBeforeDamage = false; + if ( TFGameRules() && TFGameRules()->IsPasstimeMode() ) + { + bHadBallBeforeDamage = m_Shared.HasPasstimeBall(); + } + + // damage may not come from a weapon (ie: Bosses, etc) + // The existing code below already checked for NULL pWeapon, anyways + CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( inputInfo.GetWeapon() ); + + if ( GetFlags() & FL_GODMODE ) + return 0; + + if ( IsInCommentaryMode() ) + return 0; + + bool bBuddha = ( m_debugOverlays & OVERLAY_BUDDHA_MODE ) ? true : false; + +#if defined( _DEBUG ) || defined( STAGING_ONLY ) + if ( mp_developer.GetInt() > 1 && !IsBot() ) + bBuddha = true; +#endif // _DEBUG || STAGING_ONLY + + if ( bBuddha ) + { + if ( ( m_iHealth - info.GetDamage() ) <= 0 ) + { + m_iHealth = 1; + return 0; + } + } + + if ( !IsAlive() ) + return 0; + + // Early out if there's no damage + if ( !info.GetDamage() ) + return 0; + + // Ghosts dont take damage + if ( m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) ) + { + return 0; + } + + CBaseEntity *pAttacker = info.GetAttacker(); + CTFPlayer *pTFAttacker = ToTFPlayer( pAttacker ); + + bool bDebug = tf_debug_damage.GetBool(); + + // If attacker has Strength Powerup Rune, apply damage multiplier, but not if you're a building + if ( !bIsObject && pTFAttacker && pTFAttacker->m_Shared.GetCarryingRuneType() == RUNE_STRENGTH ) + { + info.ScaleDamage( 2.f ); + } + + // Make sure the player can take damage from the attacking entity + if ( !g_pGameRules->FPlayerCanTakeDamage( this, pAttacker, info ) ) + { + if ( bDebug ) + { + Warning( " ABORTED: Player can't take damage from that attacker.\n" ); + } + + return 0; + } + + if ( IsBot() ) + { + // Don't let Sentry Busters die until they've done their spin-up + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + CTFBot *bot = ToTFBot( this ); + if ( bot ) + { + if ( bot->HasMission( CTFBot::MISSION_DESTROY_SENTRIES ) ) + { + if ( ( m_iHealth - info.GetDamage() ) <= 0 ) + { + m_iHealth = 1; + return 0; + } + } + + // Sentry Busters hurt teammates when they explode. + // Force damage value when the victim is a giant. + if ( pTFAttacker && pTFAttacker->IsBot() ) + { + CTFBot *pTFAttackerBot = ToTFBot( pTFAttacker ); + if ( pTFAttackerBot && + ( pTFAttackerBot != this ) && + pTFAttackerBot->GetPrevMission() == CTFBot::MISSION_DESTROY_SENTRIES && + info.IsForceFriendlyFire() && + InSameTeam( pTFAttackerBot ) && + IsMiniBoss() ) + { + info.SetDamage( 600.f ); + } + } + } + } + } + + // Halloween 2011 + if ( IsInPurgatory() ) + { + info.SetDamage( m_purgatoryPainMultiplier * info.GetDamage() ); + } + + m_iHealthBefore = GetHealth(); + + bool bIsSoldierRocketJumping = ( IsPlayerClass( TF_CLASS_SOLDIER ) && (pAttacker == this) && !(GetFlags() & FL_ONGROUND) && !(GetFlags() & FL_INWATER)) && (inputInfo.GetDamageType() & DMG_BLAST); + bool bIsDemomanPipeJumping = ( IsPlayerClass( TF_CLASS_DEMOMAN) && (pAttacker == this) && !(GetFlags() & FL_ONGROUND) && !(GetFlags() & FL_INWATER)) && (inputInfo.GetDamageType() & DMG_BLAST); + + if ( bDebug ) + { + Warning( "%s taking damage from %s, via %s. Damage: %.2f\n", GetDebugName(), info.GetInflictor() ? info.GetInflictor()->GetDebugName() : "Unknown Inflictor", pAttacker ? pAttacker->GetDebugName() : "Unknown Attacker", info.GetDamage() ); + } + + if ( pTFAttacker ) + { + pTFAttacker->SetLastEntityDamagedTime( gpGlobals->curtime ); + pTFAttacker->SetLastEntityDamaged( this ); + + CTFWeaponBase *myWeapon = GetActiveTFWeapon(); + CTFWeaponBase *attackerWeapon = pTFAttacker->GetActiveTFWeapon(); + + if ( myWeapon && attackerWeapon ) + { + int iStunEnemyWithSameWeapon = 0; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( attackerWeapon, iStunEnemyWithSameWeapon, stun_enemies_wielding_same_weapon ); + if ( iStunEnemyWithSameWeapon ) + { + CEconItemView *myItem = myWeapon->GetAttributeContainer()->GetItem(); + CEconItemView *attackerItem = attackerWeapon->GetAttributeContainer()->GetItem(); + + if ( myItem && attackerItem && myItem->GetItemDefIndex() == attackerItem->GetItemDefIndex() ) + { + // we're both wielding the same weapon - stun! + m_Shared.StunPlayer( 1.0f, 1.0, TF_STUN_BOTH | TF_STUN_NO_EFFECTS ); + } + } + } + } + + if ( ( info.GetDamageType() & DMG_FALL ) ) + { + bool bHitEnemy = false; + + // Are we transferring falling damage to someone else? + // Space Gravity gives everyone manntreads effect. Mantreads just makes it higher + int iHeadStomp = 0; + CALL_ATTRIB_HOOK_INT( iHeadStomp, boots_falling_stomp ); + +//#ifdef STAGING_ONLY +// if ( ( iHeadStomp || m_Shared.InCond( TF_COND_SPACE_GRAVITY ) ) && +//#else + if ( iHeadStomp && +//#endif // STAGING_ONLY + GetGroundEntity() && + GetGroundEntity()->IsPlayer() ) + { + // Did we land on a guy from the enemy team? + CTFPlayer *pOther = ToTFPlayer( GetGroundEntity() ); + if ( pOther && pOther->GetTeamNumber() != GetTeamNumber() ) + { + float flStompDamage = info.GetDamage(); + if ( iHeadStomp ) + { + flStompDamage = 10.0f + flStompDamage * 3.0f; + } + + CTakeDamageInfo infoInner( this, this, GetEquippedWearableForLoadoutSlot( LOADOUT_POSITION_SECONDARY ), flStompDamage, DMG_FALL, TF_DMG_CUSTOM_BOOTS_STOMP ); + pOther->TakeDamage( infoInner ); + m_Local.m_flFallVelocity = 0; + info.SetDamage( 0.0f ); + EmitSound( "Weapon_Mantreads.Impact" ); + UTIL_ScreenShake( pOther->WorldSpaceCenter(), 15.0, 150.0, 1.0, 500, SHAKE_START ); + + bHitEnemy = true; + } + } + +#ifdef STAGING_ONLY +/* + // no fall damage in space + if ( m_Shared.InCond( TF_COND_SPACE_GRAVITY ) ) + { + info.SetDamage( 0.0f ); + } +*/ + + // Apply an impact stun (intensity determined by fall damage for now) + if ( TFGameRules()->GameModeUsesUpgrades() && m_Shared.InCond( TF_COND_ROCKETPACK ) ) + { + float flStunTime = RemapValClamped( info.GetDamage(), 0.1f, 50.f, 1.f, 3.f ); + m_Shared.ApplyRocketPackStun( bHitEnemy ? 5.f : flStunTime ); + + info.SetDamage( 0.f ); + m_Local.m_flFallVelocity = 0.f; + } +#endif // STAGING_ONLY + } + + // Ignore damagers on our team, to prevent capturing rocket jumping, etc. + if ( pAttacker && pAttacker->GetTeam() != GetTeam() ) + { + m_AchievementData.AddDamagerToHistory( pAttacker ); + if ( pAttacker->IsPlayer() ) + { + ToTFPlayer( pAttacker )->m_AchievementData.AddTargetToHistory( this ); + + // add to list of damagers via sentry so that later we can check for achievement: ACHIEVEMENT_TF_ENGINEER_SHOTGUN_KILL_PREV_SENTRY_TARGET + CBaseEntity *pInflictor = info.GetInflictor(); + CObjectSentrygun *pSentry = dynamic_cast< CObjectSentrygun * >( pInflictor ); + if ( pSentry ) + { + m_AchievementData.AddSentryDamager( pAttacker, pInflictor ); + } + } + } + + // keep track of amount of damage last sustained + m_lastDamageAmount = info.GetDamage(); + m_LastDamageType = info.GetDamageType(); + + if ( m_LastDamageType & DMG_FALL ) + { + if ( ( m_lastDamageAmount > m_iLeftGroundHealth ) && ( m_lastDamageAmount < GetHealth() ) ) + { + // we gained health in the air, and it saved us from death. + // if any medics are healing us, they get an achievement + int iNumHealers = m_Shared.GetNumHealers(); + for ( int i=0;i<iNumHealers;i++ ) + { + CTFPlayer *pMedic = ToTFPlayer( m_Shared.GetHealerByIndex(i) ); + + // if its a medic healing us + if ( pMedic && pMedic->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + pMedic->AwardAchievement( ACHIEVEMENT_TF_MEDIC_SAVE_FALLING_TEAMMATE ); + } + } + } + } + + // Check for Demo Achievement: + // Kill a Heavy from full health with one detonation + if ( IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ) + { + if ( pTFAttacker && pTFAttacker->IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + if ( pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_PIPEBOMBLAUNCHER ) + { + // We're at full health + if ( m_iHealthBefore >= GetMaxHealth() ) + { + // Record the time + m_fMaxHealthTime = gpGlobals->curtime; + } + + // If we're still being hit in the same time window + if ( m_fMaxHealthTime == gpGlobals->curtime ) + { + // Check if the damage is fatal + int iDamage = info.GetDamage(); + if ( m_iHealth - iDamage <= 0 ) + { + pTFAttacker->AwardAchievement( ACHIEVEMENT_TF_DEMOMAN_KILL_X_HEAVIES_FULLHP_ONEDET ); + } + } + } + } + } + + if ( bIsSoldierRocketJumping || bIsDemomanPipeJumping ) + { + int nJumpType = 0; + + // If this is our own rocket, scale down the damage if we're rocket jumping + if ( bIsSoldierRocketJumping ) + { + float flDamage = info.GetDamage() * tf_damagescale_self_soldier.GetFloat(); + info.SetDamage( flDamage ); + + if ( m_iHealthBefore - flDamage > 0 ) + { + nJumpType = TF_PLAYER_ROCKET_JUMPED; + } + } + else if ( bIsDemomanPipeJumping ) + { + nJumpType = TF_PLAYER_STICKY_JUMPED; + } + + if ( nJumpType ) + { + bool bPlaySound = false; + if ( pWeapon ) + { + int iNoBlastDamage = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iNoBlastDamage, no_self_blast_dmg ) + bPlaySound = iNoBlastDamage ? true : false; + } + + SetBlastJumpState( nJumpType, bPlaySound ); + } + } + + if ( TFGameRules()->IsMannVsMachineMode() && GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + // can only bounce invaders when they are on the ground + if ( GetGroundEntity() == NULL ) + { + info.SetDamageForce( vec3_origin ); + } + } + + // 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(); + + 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) + if ( pAttacker && pAttacker->IsSolidFlagSet( FSOLID_TRIGGER ) ) + { + CTriggerHurt *pTrigger = dynamic_cast<CTriggerHurt *>( pAttacker ); + if ( pTrigger ) + { + bAllowDamage = true; + info.SetDamageCustom( TF_DMG_CUSTOM_TRIGGER_HURT ); + } + } + else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TELEFRAG ) + { + bAllowDamage = true; + } + + if ( !TFGameRules()->ApplyOnDamageModifyRules( info, this, bAllowDamage ) ) + { + return 0; + } + + // If player has Reflect Powerup, reflect damage to attacker. + // We do this here, after damage modify rules to ensure distance falloff calculations have already been made before we pass that damage back to the attacker + if ( pTFAttacker && m_Shared.GetCarryingRuneType() == RUNE_REFLECT && pTFAttacker != this && !pTFAttacker->m_Shared.IsInvulnerable() && pTFAttacker->IsAlive() ) + { + CTakeDamageInfo dmg = info; + CTFProjectile_SentryRocket *sentryRocket = dynamic_cast<CTFProjectile_SentryRocket *>( info.GetInflictor() ); + + if ( gpGlobals->curtime > m_flNextReflectZap ) // don't spam the effect for fast weapons like flamethrower and minigun + { + m_flNextReflectZap = gpGlobals->curtime + 0.5f; + + CPVSFilter filter( WorldSpaceCenter() ); + Vector vEnd = pTFAttacker->WorldSpaceCenter(); + Vector vStart = WorldSpaceCenter(); + + if ( bIsObject || sentryRocket ) + { + CBaseEntity *pInflictor = info.GetInflictor(); + vEnd = pInflictor->WorldSpaceCenter(); + } + else + { + // Push the attacker away from the Reflect powerup holder + Vector toPlayer = vEnd - vStart; + toPlayer.z = 0.0f; + toPlayer.NormalizeInPlace(); + toPlayer.z = 1.0f; + float flDamage = dmg.GetDamage(); + if ( dmg.GetDamageCustom() != TF_DMG_CUSTOM_BURNING ) + { + float flPushForce = RemapValClamped( flDamage, 0.1f, 150.f, 300.f, 500.f ); // Scale the push force according to damage + Vector vPush = flPushForce * toPlayer; + pTFAttacker->ApplyAbsVelocityImpulse( vPush ); + } + + // Play a sound and reduce the volume if damage is low + CSoundParameters params; + if ( CBaseEntity::GetParametersForSound( "Powerup.Reflect.Reflect", params, NULL ) ) + { + CPASAttenuationFilter soundFilter( pTFAttacker->GetAbsOrigin(), params.soundlevel ); + EmitSound_t ep( params ); + + if ( flDamage < 10.f ) + { + ep.m_flVolume *= 0.75f; + } + + pTFAttacker->EmitSound( soundFilter, entindex(), ep ); + pTFAttacker->PainSound( dmg ); + } + } + + te_tf_particle_effects_control_point_t controlPoint = { PATTACH_ABSORIGIN, vEnd }; + TE_TFParticleEffectComplex( filter, 0.f, "dxhr_arm_muzzleflash", vStart, QAngle( 0.f, 0.f, 0.f ), NULL, &controlPoint, pTFAttacker, PATTACH_CUSTOMORIGIN ); + } + + dmg.SetDamageCustom( TF_DMG_CUSTOM_RUNE_REFLECT ); + dmg.SetDamageType( DMG_SHOCK ); + dmg.SetAttacker( this ); + + if ( bIsObject ) + { + CBaseEntity *pInflictor = info.GetInflictor(); + dmg.SetDamage( info.GetDamage() ); + pInflictor->TakeDamage( dmg ); + } + // Sentry rockets are not included in bIsobject so we deal with them separately + else + { + if ( sentryRocket ) + { + dmg.SetDamage( info.GetDamage() ); + info.GetInflictor()->GetOwnerEntity()->TakeDamage( dmg ); + } + else + { + // Take damage unless you have Resist or Vampire (they are immune to reflect damage) + if ( pTFAttacker->m_Shared.GetCarryingRuneType() != RUNE_RESIST && pTFAttacker->m_Shared.GetCarryingRuneType() != RUNE_VAMPIRE ) + { + dmg.SetDamage(info.GetDamage() * 0.8f); + pTFAttacker->TakeDamage( dmg ); + } + } + } + } + + //Don't take damage while I'm phasing. + if ( ( m_Shared.InCond( TF_COND_PHASE ) || m_Shared.InCond( TF_COND_PASSTIME_INTERCEPTION ) ) && bAllowDamage == false ) + { + SpeakConceptIfAllowed( MP_CONCEPT_DODGE_SHOT ); + + if ( pAttacker && pAttacker->IsPlayer() ) + { + CEffectData data; + data.m_nHitBox = GetParticleSystemIndex( "miss_text" ); + data.m_vOrigin = WorldSpaceCenter() + Vector(0,0,32); + data.m_vAngles = vec3_angle; + data.m_nEntIndex = 0; + + CSingleUserRecipientFilter filter( (CBasePlayer*)pAttacker ); + te->DispatchEffect( filter, 0.0, data.m_vOrigin, "ParticleEffect", data ); + } + + Vector vecDir = vec3_origin; + if ( info.GetInflictor() ) + { + vecDir = info.GetInflictor()->WorldSpaceCenter() - Vector ( 0.0f, 0.0f, 10.0f ) - WorldSpaceCenter(); + VectorNormalize( vecDir ); + } + + ApplyPushFromDamage( info, vecDir ); + + if ( m_Shared.InCond( TF_COND_PHASE ) ) + { + m_Shared.m_ConditionData[ TF_COND_PHASE ].m_nPreventedDamageFromCondition += info.GetDamage(); + m_Shared.m_iPhaseDamage += info.GetDamage(); + } + + bTookDamage = false; + } + else + { + bool bFatal = ( m_iHealth - info.GetDamage() ) <= 0; + + bool bTrackEvent = pTFAttacker && pTFAttacker != this && !pTFAttacker->IsBot() && !IsBot(); + if ( bTrackEvent ) + { + float flHealthRemoved = bFatal ? m_iHealth : info.GetDamage(); + if ( info.GetDamageBonus() && info.GetDamageBonusProvider() ) + { + // Don't deal with raw damage numbers, only health removed. + // Example based on a crit rocket to a player with 120 hp: + // Actual damage is 120, but potential damage is 300, where + // 100 is the base, and 200 is the bonus. Apply this ratio + // to actual (so, attacker did 40, and provider added 80). + float flBonusMult = info.GetDamage() / abs( info.GetDamageBonus() - info.GetDamage() ); + float flBonus = flHealthRemoved - ( flHealthRemoved / flBonusMult ); + m_AchievementData.AddDamageEventToHistory( info.GetDamageBonusProvider(), flBonus ); + flHealthRemoved -= flBonus; + } + m_AchievementData.AddDamageEventToHistory( pAttacker, flHealthRemoved ); + } + + // This should kill us + if ( bFatal ) + { + // Damage could have been modified since we started + // Try to prevent death with buddha one more time + if ( bBuddha ) + { + m_iHealth = 1; + return 0; + } + + // Check to see if we have the cheat death attribute that makes + // us teleport to base rather than die + float flCheatDeathChance = 0.f; + CALL_ATTRIB_HOOK_FLOAT( flCheatDeathChance, teleport_instead_of_die ); + if( RandomFloat() < flCheatDeathChance ) + { + // Send back to base + ForceRespawn(); + + m_iHealth = 1; + return 0; + } + + // Avoid one death + if ( m_Shared.InCond( TF_COND_PREVENT_DEATH ) ) + { + m_Shared.RemoveCond( TF_COND_PREVENT_DEATH ); + m_iHealth = 1; + return 0; + } + + // Powerup-sourced reflected damage should not kill player + if ( info.GetDamageCustom() == TF_DMG_CUSTOM_RUNE_REFLECT ) + { + m_iHealth = 1; + return 0; + } + } + + // 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; + } + + // Check to see if we need to pass along the damage to other players + if ( pWeapon && ( gs_pRecursivePlayerCheck == NULL ) ) + { + int iDamageAllConnected = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iDamageAllConnected, damage_all_connected ); + + if ( iDamageAllConnected > 0 ) + { + // Am I healing someone or being healed? + CUtlVector<CTFPlayer*> pTempPlayerQueue; + AddConnectedPlayers( pTempPlayerQueue, this ); + + gs_pRecursivePlayerCheck = this; + for ( int iCount = 0 ; iCount < pTempPlayerQueue.Count() ; iCount++ ) + { + CTFPlayer *pTFPlayer = pTempPlayerQueue[iCount]; + if ( pTFPlayer && ( pTFPlayer != this ) ) + { + pTFPlayer->TakeDamage( inputInfo ); + } + } + gs_pRecursivePlayerCheck = NULL; + } + } + } + + if ( bTookDamage == false ) + return 0; + + if ( bDebug ) + { + Warning( " DEALT: Player took %.2f damage.\n", info.GetDamage() ); + Warning( " HEALTH LEFT: %d\n", GetHealth() ); + } + + // Some weapons have the ability to impart extra moment just because they feel like it. Let their attributes + // do so if they're in the mood. + if ( pWeapon != NULL ) + { + float flZScale = 0.0f; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pWeapon, flZScale, apply_z_velocity_on_damage ); + if ( flZScale != 0.0f ) + { + ApplyAbsVelocityImpulse( Vector( 0.0f, 0.0f, flZScale ) ); + } + + float flDirScale = 0.0f; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pWeapon, flDirScale, apply_look_velocity_on_damage ); + if ( flDirScale != 0.0f && pAttacker != NULL ) + { + Vector vecForward; + AngleVectors( pAttacker->EyeAngles(), &vecForward ); + + Vector vecForwardNoDownward = Vector( vecForward.x, vecForward.y, MIN( 0.0f, vecForward.z ) ).Normalized(); + ApplyAbsVelocityImpulse( vecForwardNoDownward * flDirScale ); + } + } + + // let weapons react to their owner being injured + CTFWeaponBase *pMyWeapon = GetActiveTFWeapon(); + if ( pMyWeapon ) + { + pMyWeapon->ApplyOnInjuredAttributes( this, pTFAttacker, info ); + } + + // Send the damage message to the client for the hud damage indicator + // 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(); + } + + // Tell the player's client that he's been hurt. + if ( m_iHealthBefore != GetHealth() ) + { + CSingleUserRecipientFilter user( this ); + UserMessageBegin( user, "Damage" ); + WRITE_SHORT( clamp( (int)info.GetDamage(), 0, 32000 ) ); + WRITE_LONG( info.GetDamageType() ); + // Tell the client whether they should show it in the indicator + if ( bitsDamage != DMG_GENERIC && !(bitsDamage & (DMG_DROWN | DMG_FALL | DMG_BURN) ) ) + { + WRITE_BOOL( true ); + WRITE_VEC3COORD( vecDamageOrigin ); + } + else + { + WRITE_BOOL( false ); + } + 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; + } + } + + const char* pzsMedigunResistEffect = NULL; + const char* pzsTeam = GetTeamNumber() == TF_TEAM_RED ? "red" : "blue"; + + // If we have one of the medigun resist buffs and get hit with the matching damage type then + // spawn a particle above our head to let enemies know their damage is being resisted, and tell + // the medic he's doing the right thing. + + bool bMedicBulletResist = m_Shared.InCond( TF_COND_MEDIGUN_UBER_BULLET_RESIST ) || m_Shared.InCond( TF_COND_MEDIGUN_SMALL_BULLET_RESIST ); + bool bMedicExplosiveResist = m_Shared.InCond( TF_COND_MEDIGUN_UBER_BLAST_RESIST ) || m_Shared.InCond( TF_COND_MEDIGUN_SMALL_BLAST_RESIST ); + bool bMedicFireResist = m_Shared.InCond( TF_COND_MEDIGUN_UBER_FIRE_RESIST ) || m_Shared.InCond( TF_COND_MEDIGUN_SMALL_FIRE_RESIST ); + + if( ( bMedicBulletResist && ( bitsDamage & DMG_BULLET ) ) ) + { + pzsMedigunResistEffect = CFmtStr( "vaccinator_%s_buff1_burst", pzsTeam ); + } + else if( bMedicExplosiveResist && ( bitsDamage & DMG_BLAST ) ) + { + pzsMedigunResistEffect = CFmtStr( "vaccinator_%s_buff2_burst", pzsTeam ); + } + else if( bMedicFireResist && ( bitsDamage & DMG_BURN ) ) + { + pzsMedigunResistEffect = CFmtStr( "vaccinator_%s_buff3_burst", pzsTeam ); + } + + if( pzsMedigunResistEffect != NULL ) + { + const Vector& vecOrigin = GetAbsOrigin(); + CPVSFilter filter( vecOrigin ); + TE_TFParticleEffect( filter, 0, pzsMedigunResistEffect, vecOrigin, vec3_angle ); + } + + // 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 reset + + // Flinch + bool bFlinch = true; + if ( bitsDamage != DMG_GENERIC ) + { + if ( IsPlayerClass( TF_CLASS_SNIPER ) && m_Shared.InCond( TF_COND_AIMING ) ) + { + if ( pTFAttacker && pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_MINIGUN ) + { + float flDistSqr = ( pTFAttacker->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); + if ( flDistSqr > 750 * 750 ) + { + bFlinch = false; + } + } + } + + if ( bFlinch ) + { + if ( ApplyPunchImpulseX( -2 ) ) + { + PlayFlinch( info ); + } + } + + // PASSTIME intense flinch to make it hard to throw straight while taking damage + extern ConVar tf_passtime_flinch_boost; + if( TFGameRules() && TFGameRules()->IsPasstimeMode() && (tf_passtime_flinch_boost.GetInt() > 0) ) + { + int iFlinch = tf_passtime_flinch_boost.GetInt(); + CTFWeaponBase *pMyWeapon = GetActiveTFWeapon(); + if( pMyWeapon && pMyWeapon->GetWeaponID() == TF_WEAPON_PASSTIME_GUN ) + { + QAngle punch; + punch.Random( -iFlinch, iFlinch ); + SetPunchAngle( punch ); + } + } + } + + // Do special explosion damage effect + if ( bitsDamage & DMG_BLAST ) + { + OnDamagedByExplosion( info ); + } + + if ( m_iHealthBefore != GetHealth() ) + { + PainSound( info ); + } + + // Detect drops below 25% health and restart expression, so that characters look worried. + int iHealthBoundary = (GetMaxHealth() * 0.25); + if ( GetHealth() <= iHealthBoundary && m_iHealthBefore > iHealthBoundary ) + { + ClearExpression(); + } + +#ifdef _DEBUG + // Report damage from the info in debug so damage against targetdummies goes + // through the system, as m_iHealthBefore - GetHealth() will always be 0. + CTF_GameStats.Event_PlayerDamage( this, info, info.GetDamage() ); +#else + CTF_GameStats.Event_PlayerDamage( this, info, m_iHealthBefore - GetHealth() ); +#endif // _DEBUG + + // if we take damage after we leave the ground, update the health if its less + if ( bTookDamage && m_iLeftGroundHealth > 0 ) + { + if ( GetHealth() < m_iLeftGroundHealth ) + { + m_iLeftGroundHealth = GetHealth(); + } + } + + if ( IsPlayerClass( TF_CLASS_SPY ) && ( inputInfo.GetDamageCustom() != TF_DMG_CUSTOM_TELEFRAG ) ) + { + // Trigger feign death if the player has it prepped... + if ( m_Shared.IsFeignDeathReady() ) + { + m_Shared.SetFeignDeathReady( false ); + if ( !m_Shared.InCond( TF_COND_TAUNTING ) ) + { + SpyDeadRingerDeath( info ); + pTFAttacker->IncrementKillCountSinceLastDeploy( info ); + } + } + else if ( !( info.GetDamageType() & DMG_FALL ) ) + { + m_Shared.NoteLastDamageTime( m_lastDamageAmount ); + } + } + + if ( pWeapon ) + { + pWeapon->ApplyPostHitEffects( inputInfo, this ); + } + + if ( IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + // Reduce charge if damage is taken + int iDemoChargeDamagePenalty = 0; + CALL_ATTRIB_HOOK_INT( iDemoChargeDamagePenalty, lose_demo_charge_on_damage_when_charging ); + // Does not apply to self or fall damage + if ( iDemoChargeDamagePenalty && m_Shared.InCond( TF_COND_SHIELD_CHARGE ) && !( info.GetDamageType() & DMG_FALL ) && (pAttacker != this) ) + { + iDemoChargeDamagePenalty *= info.GetDamage(); + m_Shared.SetDemomanChargeMeter( Max( m_Shared.GetDemomanChargeMeter() - (float)iDemoChargeDamagePenalty, 0.0f ) ); + } + } + +#ifdef STAGING_ONLY + // Remove Cond if hit + if ( m_Shared.InCond( TF_COND_NO_COMBAT_SPEED_BOOST ) ) + { + m_Shared.RemoveCond( TF_COND_NO_COMBAT_SPEED_BOOST ); + } +#endif + + float flRageScale = 1.0f; + CALL_ATTRIB_HOOK_FLOAT( flRageScale, rage_giving_scale ); + + // Give the soldier/pyro some rage points for dealing/taking damage. + if ( bTookDamage && pTFAttacker != this ) + { + // Buff flag 1: we get rage when we deal damage. Here, that means the soldier that attacked + // gets rage when we take damage. + HandleRageGain( pTFAttacker, kRageBuffFlag_OnDamageDealt, info.GetDamage() * flRageScale, 6.0f ); + + // Buff flag 2: we get rage when we take damage. + if ( !( info.GetDamageType() & DMG_FALL ) ) + { + HandleRageGain( this, kRageBuffFlag_OnDamageReceived, info.GetDamage() * flRageScale, 3.5f ); + } + + // Buff 5: our pyro attacker get rage when we're damaged by fire + if ( ( info.GetDamageType() & DMG_BURN ) != 0 || ( info.GetDamageType() & DMG_PLASMA ) != 0 ) + { + float flInverseRageGainScale = TFGameRules()->IsMannVsMachineMode() ? 12.f : 3.f; + HandleRageGain( pTFAttacker, kRageBuffFlag_OnBurnDamageDealt, info.GetDamage() * flRageScale, flInverseRageGainScale ); + } + } + + if ( pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_BAT_FISH ) + { + bool bDisguised = m_Shared.InCond( TF_COND_DISGUISED ) && (m_Shared.GetDisguiseTeam() == pTFAttacker->GetTeamNumber()); + + if ( m_iHealth <= 0 ) + { + info.SetDamageCustom( TF_DMG_CUSTOM_FISH_KILL ); + } + + if ( m_iHealth <= 0 || !bDisguised ) + { + // Do you ever find yourself typing "fish damage override" into a million-lines-of-code project and + // wondering about the world? Because I do. + int iFishDamageOverride = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iFishDamageOverride, fish_damage_override ); + + TFGameRules()->DeathNotice( this, info, iFishDamageOverride ? "fish_notice__arm" : "fish_notice" ); + } + } + + if ( IsPlayerClass( TF_CLASS_SCOUT) ) + { + // Lose hype on take damage + int iHypeResetsOnTakeDamage = 0; + CALL_ATTRIB_HOOK_INT( iHypeResetsOnTakeDamage, lose_hype_on_take_damage ); + if ( iHypeResetsOnTakeDamage != 0 ) + { + // Loose x hype on jump + float flHype = m_Shared.GetScoutHypeMeter(); + m_Shared.SetScoutHypeMeter( flHype - iHypeResetsOnTakeDamage * info.GetDamage() ); + TeamFortress_SetSpeed(); + } + } + + // Add humilation Obituary here for throwable hits + //if ( info.GetDamageCustom() == TF_DMG_CUSTOM_THROWABLE ) + //{ + // bool bDisguised = m_Shared.InCond( TF_COND_DISGUISED ) && (m_Shared.GetDisguiseTeam() == pTFAttacker->GetTeamNumber()); + + // if( m_iHealth <= 0 ) + // { + // info.SetDamageCustom( TF_DMG_CUSTOM_THROWABLE_KILL ); + // } + + // if ( m_iHealth <= 0 || !bDisguised ) + // { + // TFGameRules()->DeathNotice( this, info, "throwable_hit" ); + // } + //} + + // Let attacker react to the damage they dealt + if ( pTFAttacker ) + { + pTFAttacker->OnDealtDamage( this, info ); + } + + bool bIsPyroDetonateJumping = ( IsPlayerClass( TF_CLASS_PYRO ) && pAttacker == this && !(GetFlags() & FL_ONGROUND) && !(GetFlags() & FL_INWATER)); + if ( bIsDemomanPipeJumping || bIsSoldierRocketJumping || bIsPyroDetonateJumping ) + { + // Are we being healed by any QuickFix medics? + for ( int i = 0; i < m_Shared.m_nNumHealers; i++ ) + { + CTFPlayer *pMedic = ToTFPlayer( m_Shared.m_aHealers[i].pHealer ); + if ( !pMedic ) + continue; + + // Share blast jump with them + CWeaponMedigun *pMedigun = dynamic_cast< CWeaponMedigun* >( pMedic->GetActiveTFWeapon() ); + if ( pMedigun && pMedigun->GetMedigunType() == MEDIGUN_QUICKFIX ) + { +// Vector vecDir = vec3_origin; +// if ( info.GetInflictor() ) +// { +// vecDir = info.GetInflictor()->WorldSpaceCenter() - Vector ( 0.0f, 0.0f, 10.0f ) - WorldSpaceCenter(); +// info.GetInflictor()->AdjustDamageDirection( info, vecDir, this ); +// VectorNormalize( vecDir ); +// } +// pMedic->RemoveFlag( FL_ONGROUND ); +// pMedic->ApplyPushFromDamage( info, vecDir ); + + float flForce = GetAbsVelocity().Length(); + flForce = MIN( flForce, 900.f ); + Vector vecNewVelocity = GetAbsVelocity(); + VectorNormalize( vecNewVelocity ); + pMedic->RemoveFlag( FL_ONGROUND ); + pMedic->ApplyAbsVelocityImpulse( vecNewVelocity * flForce ); + } + } + } + + if ( pTFAttacker && pTFAttacker->IsPlayerClass( TF_CLASS_SOLDIER ) ) + { + if ( info.GetDamageType() & DMG_BLAST ) + { + // Send an event whenever a soldier hits another player directly with a stun rocket + CTFBaseRocket *pRocket = dynamic_cast< CTFBaseRocket* >( info.GetInflictor() ); + if ( pRocket && pRocket->GetStunLevel() && pRocket->GetEnemy() && pRocket->GetEnemy() == this ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "player_directhit_stun" ); + if ( event ) + { + event->SetInt( "attacker", pTFAttacker->entindex() ); + event->SetInt( "victim", entindex() ); + gameeventmanager->FireEvent( event ); + } + } + } + } + + + CTFWeaponBase *pTFWeapon = GetKilleaterWeaponFromDamageInfo( &info ); + if ( !pTFWeapon ) + { + // Check Wearable instead like demoshields or manntreads + CTFWearable *pWearable = dynamic_cast< CTFWearable* >( info.GetWeapon() ); + if ( pWearable ) + { + EconEntity_OnOwnerKillEaterEvent_Batched( pWearable, pTFAttacker, this, kKillEaterEvent_DamageDealt, info.GetDamage() ); + EconEntity_OnOwnerKillEaterEvent_Batched( pWearable, pTFAttacker, this, kKillEaterEvent_PlayersHit, 1 ); + } + } + else + { + EconEntity_OnOwnerKillEaterEvent_Batched( pTFWeapon, pTFAttacker, this, kKillEaterEvent_DamageDealt, info.GetDamage() ); + EconEntity_OnOwnerKillEaterEvent_Batched( pTFWeapon, pTFAttacker, this, kKillEaterEvent_PlayersHit, 1 ); + } + + // bHadBallBeforeDamage will always be false in non-passtime modes + if ( bTookDamage && bHadBallBeforeDamage ) + { + g_pPasstimeLogic->OnBallCarrierDamaged( this, pTFAttacker, info ); + } + + return info.GetDamage(); +} + +//----------------------------------------------------------------------------- +// Purpose: Invoked when we deal damage to another victim +//----------------------------------------------------------------------------- +void CTFPlayer::OnDealtDamage( CBaseCombatCharacter *pVictim, const CTakeDamageInfo &info ) +{ + if ( pVictim ) + { + // which second of the window are we in + int i = (int)gpGlobals->curtime; + i %= DPS_Period; + + if ( i != m_lastDamageRateIndex ) + { + // a second has ticked over, start a new accumulation + m_damageRateArray[ i ] = info.GetDamage(); + m_lastDamageRateIndex = i; + + // track peak DPS for this player + m_peakDamagePerSecond = 0; + for( i=0; i<DPS_Period; ++i ) + { + if ( m_damageRateArray[i] > m_peakDamagePerSecond ) + { + m_peakDamagePerSecond = m_damageRateArray[i]; + } + } + } + else + { + m_damageRateArray[ i ] += info.GetDamage(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::AddConnectedPlayers( CUtlVector<CTFPlayer*> &vecPlayers, CTFPlayer *pPlayerToConsider ) +{ + if ( !pPlayerToConsider ) + return; + + if ( vecPlayers.Find( pPlayerToConsider ) != vecPlayers.InvalidIndex() ) + return; // already in the list + + vecPlayers.AddToTail( pPlayerToConsider ); + + if ( pPlayerToConsider->MedicGetHealTarget() ) + { + AddConnectedPlayers( vecPlayers, ToTFPlayer( pPlayerToConsider->MedicGetHealTarget() ) ); + } + + for ( int i = 0 ; i < pPlayerToConsider->m_Shared.GetNumHealers() ; i++ ) + { + CTFPlayer *pMedic = ToTFPlayer( pPlayerToConsider->m_Shared.GetHealerByIndex( i ) ); + if ( pMedic ) + { + AddConnectedPlayers( vecPlayers, pMedic ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Reduces backstab damage if we have a back shield. +//----------------------------------------------------------------------------- +bool CTFPlayer::CheckBlockBackstab( CTFPlayer *pTFAttacker ) +{ + // Check all items for the attribute that blocks a backstab. + // Destroy the first item that intercepts the backstab. + CUtlVector<CBaseEntity*> itemList; + int iBackStabShield = 0; + CALL_ATTRIB_HOOK( int, iBackStabShield, set_blockbackstab_once, this, &itemList ); + if ( iBackStabShield ) + { + Assert( itemList.Count() != 0 ); + CBaseEntity *pEntity = itemList.Element( 0 ); + if ( pEntity ) + { + if ( pEntity->IsBaseCombatWeapon() ) + { + // Remove. + } + + if ( pEntity->IsWearable() ) + { + // Yay stats. + EconEntity_OnOwnerKillEaterEvent( dynamic_cast<CEconEntity *>( pEntity ), this, pTFAttacker, kKillEaterEvent_BackstabAbsorbed ); + + // Unequip. + CTFWearable *pItem = dynamic_cast<CTFWearable *>( pEntity ); + pItem->Break(); + pItem->RemoveFrom( this ); + } + + UTIL_Remove( pEntity ); + + // tell the bot his Razorback just got broken + CTFBot *me = ToTFBot( this ); + if ( me ) + { + me->DelayedThreatNotice( pTFAttacker, 0.5f ); + } + } + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// 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 || collisionGroup == TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_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; + } + + if ( TFGameRules()->ShowMatchSummary() ) + return; + + // No suicide while a ghost! + if ( m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) ) + return; + + // No suicide while a kart + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + return; + + m_bSuicideExplode = bExplode; + m_iSuicideCustomKillFlags = TF_DMG_CUSTOM_SUICIDE; + + BaseClass::CommitSuicide( bExplode, bForce ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &info - +// Output : int +//----------------------------------------------------------------------------- +ConVar tf_preround_push_from_damage_enable( "tf_preround_push_from_damage_enable", "0", FCVAR_NONE, "If enabled, this will allow players using certain type of damage to move during pre-round freeze time." ); +void CTFPlayer::ApplyPushFromDamage( const CTakeDamageInfo &info, Vector vecDir ) +{ + // check if player can be moved + if ( !tf_preround_push_from_damage_enable.GetBool() && !CanPlayerMove() ) + return; + + if ( m_bIsTargetDummy ) + return; + + Vector vecForce; + vecForce.Init(); + if ( info.GetAttacker() == this ) + { + Vector vecSize = WorldAlignSize(); + Vector hullSizeCrouch = VEC_DUCK_HULL_MAX - VEC_DUCK_HULL_MIN; + + if ( vecSize == hullSizeCrouch ) + { + // Use the original hull for damage force calculation to ensure our RJ height doesn't change due to crouch hull increase + // ^^ Comment above is an ancient lie, Ducking actually increases blast force, this value increases it even more 82 standing, 62 ducking, 55 modified + vecSize.z = 55; + } + + float flDamageForForce = info.GetDamageForForceCalc() ? info.GetDamageForForceCalc() : info.GetDamage(); + + float flSelfPushMult = 1.0; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( info.GetWeapon(), flSelfPushMult, mult_dmgself_push_force ); + + + if ( IsPlayerClass( TF_CLASS_SOLDIER ) ) + { + // Rocket Jump + if ( (info.GetDamageType() & DMG_BLAST) ) + { + if ( GetFlags() & FL_ONGROUND ) + { + vecForce = vecDir * -DamageForce( vecSize, flDamageForForce, tf_damageforcescale_self_soldier_badrj.GetFloat() ) * flSelfPushMult; + } + else + { + vecForce = vecDir * -DamageForce( vecSize, flDamageForForce, tf_damageforcescale_self_soldier_rj.GetFloat() ) * flSelfPushMult; + } + + SetBlastJumpState( TF_PLAYER_ROCKET_JUMPED ); + + // Reset duck in air on self rocket impulse. + m_Shared.SetAirDucked( 0 ); + } + else + { + // Self Damage no force + vecForce.Zero(); + } + + } + else + { + // Detonator blast jump modifier + if ( IsPlayerClass( TF_CLASS_PYRO ) && info.GetDamageCustom() == TF_DMG_CUSTOM_FLARE_EXPLOSION ) + { + vecForce = vecDir * -DamageForce( vecSize, flDamageForForce, tf_damageforcescale_pyro_jump.GetFloat() ) * flSelfPushMult; + } + else + { + // Other Jumps (Stickies) + vecForce = vecDir * -DamageForce( vecSize, flDamageForForce, DAMAGE_FORCE_SCALE_SELF ) * flSelfPushMult; + } + + // Reset duck in air on self grenade impulse. + m_Shared.SetAirDucked( 0 ); + } + // Precision removes self damage so we don't want push force from damage + if ( m_Shared.GetCarryingRuneType() == RUNE_PRECISION ) + { + vecForce.Zero(); + } + } + else + { + // Don't let bot get pushed while they're in spawn area + if ( m_Shared.InCond( TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED ) || m_Shared.GetCarryingRuneType() == RUNE_KNOCKOUT ) + { + return; + } + + // Sentryguns push a lot harder + if ( (info.GetDamageType() & DMG_BULLET) && info.GetInflictor() && info.GetInflictor()->IsBaseObject() ) + { + float flSentryPushMultiplier = 16.f; + CObjectSentrygun* pSentry = dynamic_cast<CObjectSentrygun*>( info.GetInflictor() ); + if ( pSentry ) + { + flSentryPushMultiplier = pSentry->GetPushMultiplier(); + + // Scale the force based on Distance, Wrangled Sentries should not push so hard at distance + // get the distance between sentry and victim and lower push force if outside of attack range (wrangled) + float flDistSqr = (pSentry->GetAbsOrigin() - GetAbsOrigin()).LengthSqr(); + if ( flDistSqr > SENTRY_MAX_RANGE_SQRD ) + { + flSentryPushMultiplier *= 0.5f; + } + } + vecForce = vecDir * -DamageForce( WorldAlignSize(), info.GetDamage(), flSentryPushMultiplier ); + } + else + { + CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase*>(info.GetWeapon()); + if ( pWeapon && (pWeapon->GetWeaponID() == TF_WEAPON_COMPOUND_BOW) ) + { + vecForce = vecDir * -DamageForce( WorldAlignSize(), info.GetDamage(), tf_damageforcescale_other.GetFloat() ); + vecForce.z = 0; + } + else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_PLASMA_CHARGED ) + { + vecForce = vecDir * -DamageForce( WorldAlignSize(), info.GetDamage(), tf_damageforcescale_other.GetFloat() ) * 1.25f; + } + else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_FLARE_PELLET) + { + float flTimeAlive = 0.0f; + CTFProjectile_Flare *pFlare = dynamic_cast< CTFProjectile_Flare* >( info.GetInflictor() ); + if ( pFlare ) + { + flTimeAlive = pFlare->GetTimeAlive(); + } + vecForce = vecDir * -DamageForce( WorldAlignSize(), info.GetDamage(), TF_FLARE_PELLET_FORCE * RemapValClamped( flTimeAlive, 0.1f, 1.0f, 1.0f, TF_FLARE_PELLET_FORCE_DISTANCE_SCALE ) ); + vecForce.z = ( ( GetPlayerClass()->GetClassIndex() == TF_CLASS_HEAVYWEAPONS ) ? ( TF_FLARE_PELLET_FORCE_UPWARD_HEAVY ) : ( TF_FLARE_PELLET_FORCE_UPWARD ) ); + } + else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_KART ) + { + vecForce = info.GetDamageForce(); + } + 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; + } + + CBaseEntity* pInflictor = info.GetInflictor(); + if ( pInflictor && CanScatterGunKnockBack(pWeapon, info.GetDamage(), (WorldSpaceCenter() - pInflictor->WorldSpaceCenter()).LengthSqr() ) ) + { + // Remove all Z force from these shots if they are close enough and doing enough damage + if ( vecForce.z < 0 ) + { + vecForce.z = 0; + } + } + + int iAirBlast = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iAirBlast, damage_causes_airblast ); + if ( iAirBlast ) + { + float force = -DamageForce( WorldAlignSize(), 100, 6 ); + ApplyAirBlastImpulse( force * vecDir ); + vecForce.Zero(); + } + } + + bool bBigKnockback = false; + + CTFPlayer *pAttacker = ToTFPlayer( info.GetAttacker() ); + if ( pAttacker && pAttacker->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && pAttacker->m_Shared.IsRageDraining() ) + { + // Generic Rage attribute + int iRage = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pAttacker, iRage, generate_rage_on_dmg ); + if ( iRage ) + { + // In MvM, Heavies can purchase a knockback+stun effect + float flPushMultiplier = ( iRage + 1 ) * 24.f; + vecForce = vecDir * -DamageForce( WorldAlignSize(), info.GetDamage(), flPushMultiplier ); + bBigKnockback = true; + + // Track for achievements + m_AchievementData.AddPusherToHistory( pAttacker ); + } + } + + // Airblast effect for general attacks. Scaled by range. + float flImpactBlastForce = 1.f; + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), flImpactBlastForce, damage_blast_push ); + if ( flImpactBlastForce != 1.f ) + { + CBaseEntity *pInflictor = info.GetInflictor(); + if ( pInflictor ) + { + const float flMaxPushBackDistSqr = 700.f * 700.f; + float flDistSqr = ( WorldSpaceCenter() - pInflictor->WorldSpaceCenter() ).LengthSqr(); + if ( flDistSqr <= flMaxPushBackDistSqr ) + { + if ( vecForce.z < 0 ) + { + vecForce.z = 0; + } + + m_Shared.StunPlayer( 0.3f, 1.f, TF_STUN_MOVEMENT | TF_STUN_MOVEMENT_FORWARD_ONLY, pAttacker ); + flImpactBlastForce = RemapValClamped( flDistSqr, 1000.f, flMaxPushBackDistSqr, flImpactBlastForce, ( flImpactBlastForce * 0.5f ) ); + float flForce = -DamageForce( WorldAlignSize(), info.GetDamage() * 2, flImpactBlastForce ); + ApplyAirBlastImpulse( flForce * vecDir ); + } + } + } + + if ( TFGameRules()->GameModeUsesUpgrades() ) + { + if ( GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + // invading bots can't be pushed by sentry guns + if ( info.GetInflictor() && info.GetInflictor()->IsBaseObject() ) + { + return; + } + } + + if ( GetTeamNumber() == TF_TEAM_PVE_INVADERS && !bBigKnockback ) + { + if ( IsMiniBoss() ) + { + // Minibosses can't be pushed by anything except heavy rage and airblast (airblast is suppressed when deploying in deploy ai code) + return; + } + else if ( m_nDeployingBombState != TF_BOMB_DEPLOYING_NONE && ( info.GetDamageType() & DMG_BLAST ) == 0 ) + { + // Regular robots only get pushed by blast damage when deploying the bomb + return; + } + } + } + + float flDamageForceReduction = 1.f; + CALL_ATTRIB_HOOK_FLOAT( flDamageForceReduction, damage_force_reduction ); + vecForce *= flDamageForceReduction; + } + + ApplyAbsVelocityImpulse( vecForce ); + + // If we were pushed by an enemy explosion, we're now marked as being blasted by an enemy. + // If we stay on the ground, next frame our player think will remove this flag. + if ( info.GetAttacker() != this && info.GetDamageType() & DMG_BLAST ) + { + m_bTakenBlastDamageSinceLastMovement = true; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::PlayDamageResistSound( float flStartDamage, float flModifiedDamage ) +{ + if ( flStartDamage <= 0.f ) + return; + + // Spam control + if ( gpGlobals->curtime - m_flLastDamageResistSoundTime <= 0.1f ) + return; + + // Play an absorb sound based on the percentage the damage has been reduced to + float flDamagePercent = flModifiedDamage / flStartDamage; + if ( flDamagePercent > 0.f && flDamagePercent < 1.f ) + { + const char *pszSoundName = ( flDamagePercent >= 0.75f ) ? "Player.ResistanceLight" : + ( flDamagePercent <= 0.25f ) ? "Player.ResistanceHeavy" : "Player.ResistanceMedium"; + + CSoundParameters params; + if ( CBaseEntity::GetParametersForSound( pszSoundName, params, NULL ) ) + { + CPASAttenuationFilter filter( GetAbsOrigin(), params.soundlevel ); + EmitSound_t ep( params ); + ep.m_flVolume *= RemapValClamped( flStartDamage, 1.f, 70.f, 0.7f, 1.f ); + EmitSound( filter, entindex(), ep ); + m_flLastDamageResistSoundTime = gpGlobals->curtime; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &info - +// Output : int +//----------------------------------------------------------------------------- +int CTFPlayer::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + if ( TFGameRules()->IsInItemTestingMode() && !IsFakeClient() ) + return 0; + + bool bUsingUpgrades = TFGameRules()->GameModeUsesUpgrades(); + + // Always NULL check this below + CTFPlayer *pTFAttacker = ToTFPlayer( info.GetAttacker() ); + + CTFGameRules::DamageModifyExtras_t outParams; + outParams.bIgniting = false; + outParams.bSelfBlastDmg = false; + outParams.bSendPreFeignDamage = false; + outParams.bPlayDamageReductionSound = false; + float realDamage = info.GetDamage(); + int iPreFeignDamage = realDamage; + if ( TFGameRules() ) + { + realDamage = TFGameRules()->ApplyOnDamageAliveModifyRules( info, this, outParams ); + + if ( realDamage == -1 ) + { + // Hard out requested from ApplyOnDamageModifyRules + return 0; + } + } + + if ( outParams.bPlayDamageReductionSound ) + { + PlayDamageResistSound( info.GetDamage(), realDamage ); + } + + // 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(); + info.GetInflictor()->AdjustDamageDirection( info, vecDir, this ); + VectorNormalize( vecDir ); + } + g_vecAttackDir = vecDir; + + // Do the damage. + m_bitsDamageType |= info.GetDamageType(); + + // Check to see if the Wheatley sapper item is equipped and should react + if ( m_bitsDamageType & DMG_BULLET && IsPlayerClass( TF_CLASS_SPY ) ) + { + CBaseCombatWeapon *pRet = GetActiveWeapon(); + CTFWeaponSapper *pSap = dynamic_cast< CTFWeaponSapper* >( pRet ); + if ( pSap != NULL ) + { + if (pSap->IsWheatleySapper()) + { + pSap->WheatleyDamage(); + } + } + } + + float flBleedingTime = 0.0f; + int iPrevHealth = m_iHealth; + + if ( m_takedamage != DAMAGE_EVENTS_ONLY ) + { + if ( info.GetDamageCustom() != TF_DMG_CUSTOM_BLEEDING && !outParams.bSelfBlastDmg ) + { + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( info.GetWeapon(), flBleedingTime, bleeding_duration ); + } + + // Take damage - round to the nearest integer. + int iOldHealth = m_iHealth; + m_iHealth -= ( realDamage + 0.5f ); + + if ( IsHeadshot( info.GetDamageCustom() ) && (m_iHealth <= 0) && (iOldHealth != 1) ) + { + int iNoDeathFromHeadshots = 0; + CALL_ATTRIB_HOOK_INT( iNoDeathFromHeadshots, no_death_from_headshots ); + if ( iNoDeathFromHeadshots == 1 ) + { + m_iHealth = 1; + } + } + + // For lifeleech, calculate how much damage we actually inflicted. + CTFPlayer *pAttackingPlayer = dynamic_cast<CTFPlayer *>( info.GetAttacker() ); + if ( pAttackingPlayer && pAttackingPlayer->GetActiveWeapon() ) + { + float fLifeleechOnDamage = 0.0f; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pAttackingPlayer->GetActiveWeapon(), fLifeleechOnDamage, lifeleech_on_damage ); + if ( fLifeleechOnDamage > 0.0f ) + { + const float fActualDamageDealt = iOldHealth - m_iHealth; + const float fHealAmount = fActualDamageDealt * fLifeleechOnDamage; + + if ( fHealAmount >= 0.5f ) + { + const int iHealthToAdd = MIN( (int)(fHealAmount + 0.5f), pAttackingPlayer->m_Shared.GetMaxBuffedHealth() - pAttackingPlayer->GetHealth() ); + pAttackingPlayer->TakeHealth( iHealthToAdd, DMG_GENERIC ); + } + } + } + + // track accumulated sentry gun damage dealt by players + if ( pTFAttacker ) + { + // track amount of damage dealt by defender's sentry guns + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() ); + CTFProjectile_SentryRocket *sentryRocket = dynamic_cast< CTFProjectile_SentryRocket * >( info.GetInflictor() ); + + if ( ( sentry && !sentry->IsDisposableBuilding() ) || sentryRocket ) + { + int flooredHealth = clamp( m_iHealth, 0, m_iHealth ); + + pTFAttacker->AccumulateSentryGunDamageDealt( iOldHealth - flooredHealth ); + } + } + } + + 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 ) ) ) + { + if ( !m_Shared.InCond( TF_COND_MEGAHEAL ) || outParams.bSelfBlastDmg ) + { + ApplyPushFromDamage( info, vecDir ); + } + } + } + + if ( outParams.bIgniting && pTFAttacker ) + { + m_Shared.Burn( pTFAttacker, dynamic_cast< CTFWeaponBase * >( info.GetWeapon() ) ); + } + + if ( flBleedingTime > 0 && pTFAttacker ) + { + m_Shared.MakeBleed( pTFAttacker, dynamic_cast< CTFWeaponBase * >( info.GetWeapon() ), flBleedingTime ); + } + + // Don't recieve reflected damage if you are carrying Reflect (prevents a loop in a game with two Reflect players) + if ( ( info.GetDamageType() & TF_DMG_CUSTOM_RUNE_REFLECT ) && m_Shared.GetCarryingRuneType() == RUNE_REFLECT ) + { + return 0; + } + + CTFWeaponBase *pTFWeapon = dynamic_cast< CTFWeaponBase * >( info.GetWeapon() ); + if ( pTFWeapon && WeaponID_IsSniperRifle( pTFWeapon->GetWeaponID() ) ) + { + CTFSniperRifle *pSniper = dynamic_cast<CTFSniperRifle*>( pTFWeapon ); + if ( pSniper && ( pSniper->IsZoomed() || ( pSniper->GetWeaponID() == TF_WEAPON_SNIPERRIFLE_CLASSIC ) ) ) + { + float flJarateTime = pSniper->GetJarateTime(); + if ( flJarateTime && !m_Shared.IsInvulnerable() && !m_Shared.InCond( TF_COND_PHASE ) && !m_Shared.InCond( TF_COND_PASSTIME_INTERCEPTION ) ) + { + Vector vecOrigin = info.GetDamagePosition(); + CPVSFilter filter( vecOrigin ); + TE_TFParticleEffect( filter, 0.0, "peejar_impact_small", vecOrigin, vec3_angle ); + m_Shared.AddCond( TF_COND_URINE, flJarateTime ); + + if ( pTFAttacker ) + { + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" triggered \"%s\" against \"%s<%i><%s><%s>\" with \"%s\" (attacker_position \"%d %d %d\") (victim_position \"%d %d %d\")\n", + pTFAttacker->GetPlayerName(), + pTFAttacker->GetUserID(), + pTFAttacker->GetNetworkIDString(), + pTFAttacker->GetTeam()->GetName(), + "jarate_attack", + GetPlayerName(), + GetUserID(), + GetNetworkIDString(), + GetTeam()->GetName(), + "sniperrifle", + (int)pTFAttacker->GetAbsOrigin().x, + (int)pTFAttacker->GetAbsOrigin().y, + (int)pTFAttacker->GetAbsOrigin().z, + (int)GetAbsOrigin().x, + (int)GetAbsOrigin().y, + (int)GetAbsOrigin().z ); + + // explosive jarate shot for a fully charged shot or headshot + if ( pSniper->IsFullyCharged() || IsHeadshot( info.GetDamageCustom() ) || LastHitGroup() == HITGROUP_HEAD ) + { + JarExplode( entindex(), pTFAttacker, pTFWeapon, pTFWeapon, info.GetDamagePosition(), pTFAttacker->GetTeamNumber(), 100.f, TF_COND_URINE, flJarateTime, "peejar_impact" ); + } + } + } + + if ( bUsingUpgrades && pTFAttacker ) + { + int iExplosiveShot = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER ( pTFAttacker, iExplosiveShot, explosive_sniper_shot ); + if ( iExplosiveShot ) + { + if ( IsHeadshot( info.GetDamageCustom() ) || ( flJarateTime && LastHitGroup() == HITGROUP_HEAD ) ) + { + pSniper->ExplosiveHeadShot( pTFAttacker, this ); + } + } + } + } + } + + // Prevents a sandwich ignore-ammo-while-taking-damage-and-eating alias exploit + if ( m_Shared.InCond( TF_COND_TAUNTING ) && m_Shared.GetTauntIndex() == TAUNT_BASE_WEAPON ) + { + if ( IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ) + { + CTFLunchBox *pLunchBox = dynamic_cast <CTFLunchBox *> ( m_Shared.GetActiveTFWeapon() ); + if ( pLunchBox ) + { + if ( pLunchBox->GetLunchboxType() != LUNCHBOX_ADDS_MAXHEALTH ) + { + pLunchBox->DrainAmmo( true ); + } + } + } + } + + // 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 ); + + int iDamageAmount = ( iPrevHealth - m_iHealth ); + event->SetInt( "damageamount", outParams.bSendPreFeignDamage ? iPreFeignDamage : iDamageAmount ); + + // Hurt by another player. + if ( pAttacker->IsPlayer() ) + { + CBasePlayer *pPlayer = ToBasePlayer( pAttacker ); + event->SetInt( "attacker", pPlayer->GetUserID() ); + + event->SetInt( "custom", info.GetDamageCustom() ); + event->SetBool( "showdisguisedcrit", m_bShowDisguisedCrit ); + event->SetBool( "crit", (info.GetDamageType() & DMG_CRITICAL) != 0 ); + event->SetBool( "minicrit", m_bMiniCrit ); + event->SetBool( "allseecrit", m_bAllSeeCrit ); + Assert( (int)m_eBonusAttackEffect < 256 ); + event->SetInt( "bonuseffect", (int)m_eBonusAttackEffect ); + + if ( pTFAttacker && pTFAttacker->GetActiveTFWeapon() ) + { + event->SetInt( "weaponid", pTFAttacker->GetActiveTFWeapon()->GetWeaponID() ); + } + } + // Hurt by world. + else + { + event->SetInt( "attacker", 0 ); + } + + gameeventmanager->FireEvent( event ); + } + + if ( pTFAttacker && pTFAttacker != this ) + { + pTFAttacker->RecordDamageEvent( info, (m_iHealth <= 0), iPrevHealth ); + } + + //No bleeding while invul or disguised. + bool bBleed = ( ( m_Shared.InCond( TF_COND_DISGUISED ) == false || m_Shared.GetDisguiseTeam() != pAttacker->GetTeamNumber() ) + && !m_Shared.IsInvulnerable() ); + + // No bleed effects for DMG_GENERIC + if ( info.GetDamageType() == 0 ) + { + bBleed = false; + } + + // Except if we are really bleeding! + bBleed |= m_Shared.InCond( TF_COND_BLEEDING ); + + if ( bBleed && pTFAttacker ) + { + CTFWeaponBase *pWeapon = pTFAttacker->GetActiveTFWeapon(); + if ( pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_FLAMETHROWER ) + { + bBleed = false; + } + } + + if ( bBleed && ( realDamage > 0.f ) ) + { + Vector vDamagePos = info.GetDamagePosition(); + + if ( vDamagePos == vec3_origin ) + { + vDamagePos = WorldSpaceCenter(); + } + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + if ( ( IsMiniBoss() && static_cast< float >( GetHealth() ) / GetMaxHealth() > 0.3f ) || realDamage < 50 ) + { + DispatchParticleEffect( "bot_impact_light", GetAbsOrigin(), vec3_angle ); + } + else + { + DispatchParticleEffect( "bot_impact_heavy", GetAbsOrigin(), vec3_angle ); + } + } + else + { + CPVSFilter filter( vDamagePos ); + TE_TFBlood( filter, 0.0, vDamagePos, -vecDir, entindex() ); + } + } + + if ( m_bIsTargetDummy ) + { + // In the case of a targetdummy bot, restore any damage so it can never die + TakeHealth( ( iPrevHealth - m_iHealth ), DMG_GENERIC ); + } + + m_vecFeignDeathVelocity = GetAbsVelocity(); + + if ( pTFAttacker ) + { + // If we're invuln, give whomever provided it rewards/credit + if ( m_Shared.IsInvulnerable() && realDamage > 0.f ) + { + // Medigun? + CBaseEntity *pProvider = m_Shared.GetConditionProvider( TF_COND_INVULNERABLE ); + if ( !pProvider && bUsingUpgrades ) + { + // Bottle? + pProvider = m_Shared.GetConditionProvider( TF_COND_INVULNERABLE_USER_BUFF ); + } + + if ( pProvider ) + { + CTFPlayer *pTFProvider = ToTFPlayer( pProvider ); + if ( pTFProvider ) + { + if ( pTFProvider != pTFAttacker && bUsingUpgrades ) + { + HandleRageGain( pTFProvider, kRageBuffFlag_OnHeal, ( realDamage / 2.f ), 1.f ); + } + + CTF_GameStats.Event_PlayerBlockedDamage( pTFProvider, realDamage ); + } + } + } + + // Give the attacker's medic Energy based on damage done + CBaseEntity *pProvider = pTFAttacker->m_Shared.GetConditionProvider( TF_COND_HEALTH_BUFF ); + if ( pProvider ) + { + CTFPlayer *pTFProvider = ToTFPlayer( pProvider ); + if ( pTFProvider && pTFProvider->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + // Cap to prevent insane values coming from headshots and backstabs + float flAmount = Min( realDamage, 250.f ) / 10.f; + HandleRageGain( ToTFPlayer( pProvider ), kRageBuffFlag_OnHeal, flAmount, 1.f ); + } + } + } + + // Done. + return 1; +} + +//----------------------------------------------------------------------------- +// 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.GetInt() != 1 ) + { + if ( tf_playergib.GetInt() < 1 ) + return false; + else + return true; + } + + // normal players/bots don't gib in MvM + if ( TFGameRules()->IsMannVsMachineMode() ) + return false; + + // Suicide explode always gibs. + if ( m_bSuicideExplode ) + { + m_bSuicideExplode = false; + return true; + } + + // Are we set up to gib always on critical hits? + if ( info.GetDamageType() & DMG_CRITICAL ) + { + int iAlwaysGibOnCrit = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iAlwaysGibOnCrit, crit_kill_will_gib ); + if ( iAlwaysGibOnCrit ) + return true; + } + + int iCritOnHardHit = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iCritOnHardHit, crit_on_hard_hit ); + if ( iCritOnHardHit == 0 ) + { + // Only blast & half falloff damage can gib. + if ( ( (info.GetDamageType() & DMG_BLAST) == 0 ) && + ( (info.GetDamageType() & DMG_HALF_FALLOFF) == 0 ) ) + return false; + } + + // Explosive crits always gib. + if ( info.GetDamageType() & DMG_CRITICAL ) + return true; + + // Hard hits also gib. + if ( GetHealth() <= -10 ) + return true; + + if ( m_bGoingFeignDeath ) + { + // The player won't actually have negative health, + // but spies often gib from explosive damage so we should make that likely here. + float frand = (float) rand() / VALVE_RAND_MAX; + return (frand>0.15f) ? true : false; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::HasBombinomiconEffectOnDeath( void ) +{ + int iBombinomicomEffectOnDeath = 0; + CALL_ATTRIB_HOOK_INT( iBombinomicomEffectOnDeath, bombinomicon_effect_on_death ); + + return ( iBombinomicomEffectOnDeath != 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Figures out if there is a special assist responsible for our death. +// Must be called before conditions are cleared druing death. +//----------------------------------------------------------------------------- +void CTFPlayer::DetermineAssistForKill( const CTakeDamageInfo &info ) +{ + CTFPlayer *pPlayerAttacker = ToTFPlayer( info.GetAttacker() ); + if ( !pPlayerAttacker ) + return; + + CTFPlayer *pPlayerAssist = NULL; + + if ( m_Shared.GetConditionAssistFromVictim() ) + { + // If we are covered in urine, mad milk, etc, then give the provider an assist. + pPlayerAssist = ToTFPlayer( m_Shared.GetConditionAssistFromVictim() ); + } + + if ( m_Shared.IsControlStunned() ) + { + // If we've been stunned, the stunner gets credit for the assist. + pPlayerAssist = m_Shared.GetStunner(); + } + + // Can't assist ourself. + if ( pPlayerAttacker && (pPlayerAttacker != pPlayerAssist) ) + { + m_Shared.SetAssist( pPlayerAssist ); + } + else + { + m_Shared.SetAssist( NULL ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ) +{ + BaseClass::Event_KilledOther( pVictim, info ); + + if ( pVictim->IsPlayer() ) + { + CTFPlayer *pTFVictim = ToTFPlayer( pVictim ); + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + if ( pTFVictim && pTFVictim->IsBot() && ( pTFVictim->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) ) + { + if ( pTFVictim->GetDeployingBombState() > TF_BOMB_DEPLOYING_NONE ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_kill_robot_delivering_bomb" ); + if ( event ) + { + event->SetInt( "player", entindex() ); + gameeventmanager->FireEvent( event ); + } + } + } + } + + // Custom death handlers + // TODO: Need a system here! This conditional is getting pretty big. + const char *pszCustomDeath = "customdeath:none"; + if ( info.GetAttacker() && info.GetAttacker()->IsBaseObject() ) + { + pszCustomDeath = "customdeath:sentrygun"; + } + else if ( info.GetInflictor() && info.GetInflictor()->IsBaseObject() ) + { + CBaseObject* pObj = dynamic_cast<CBaseObject*>( info.GetInflictor() ); + if ( pObj->IsMiniBuilding() ) + { + pszCustomDeath = "customdeath:minisentrygun"; + } + else + { + pszCustomDeath = "customdeath:sentrygun"; + } + } + else if ( IsHeadshot( info.GetDamageCustom() ) ) + { + 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"; + } + else if ( IsTauntDmg( info.GetDamageCustom() ) ) + { + pszCustomDeath = "customdeath:taunt"; + } + else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BURNING_FLARE ) + { + pszCustomDeath = "customdeath:flareburn"; + } + + // 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"; + } + + const char *pszVictimStunned = "victimstunned:0"; + if ( pTFVictim->m_Shared.InCond( TF_COND_STUNNED ) ) + { + pszVictimStunned = "victimstunned:1"; + } + + const char *pszVictimDoubleJumping = "victimdoublejumping:0"; + if ( pTFVictim->m_Shared.GetAirDash() > 0 ) + { + pszVictimDoubleJumping = "victimdoublejumping:1"; + } + + CFmtStrN<128> modifiers( "%s,%s,%s,%s,victimclass:%s", pszCustomDeath, pszDomination, pszVictimStunned, pszVictimDoubleJumping, g_aPlayerClassNames_NonLocalized[ pTFVictim->GetPlayerClass()->GetClassIndex() ] ); + + bool bPlayspeech = true; + + // Don't play speech if this kill disguises the spy + if ( IsPlayerClass( TF_CLASS_SPY ) ) + { + if ( !Q_stricmp( "customdeath:backstab", pszCustomDeath ) ) + { + CTFKnife *pKnife = dynamic_cast<CTFKnife *>( GetActiveTFWeapon() ); + if ( pKnife && pKnife->GetKnifeType() == KNIFE_DISGUISE_ONKILL ) + { + bPlayspeech = false; + } + } + } + + if ( bPlayspeech ) + { + SpeakConceptIfAllowed( MP_CONCEPT_KILLED_PLAYER, modifiers ); + } + + CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>(info.GetWeapon()); + if ( pWeapon ) + { + pWeapon->OnPlayerKill( pTFVictim, info ); + + int iCritBoost = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iCritBoost, add_onkill_critboost_time ); + if ( iCritBoost ) + { + // Perceptually, people seem to think the effect is shorter than the stated time, so we cheat by adding a tad more for that + m_Shared.AddCond( TF_COND_CRITBOOSTED_ON_KILL, iCritBoost+1 ); + } + + int iMiniCritBoost = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iMiniCritBoost, add_onkill_minicritboost_time ); + if ( iMiniCritBoost ) + { + // Perceptually, people seem to think the effect is shorter than the stated time, so we cheat by adding a tad more for that + m_Shared.AddCond( TF_COND_ENERGY_BUFF, iMiniCritBoost + 1 ); + } + } + + // Check for CP_Foundry achievements + if ( FStrEq( "cp_foundry", STRING( gpGlobals->mapname ) ) ) + { + if ( pTFVictim && ( pTFVictim->GetTeamNumber() != GetTeamNumber() ) ) + { + if ( pTFVictim->IsCapturingPoint() ) + { + if ( info.GetDamageType() & DMG_CRITICAL ) + { + AwardAchievement( ACHIEVEMENT_TF_MAPS_FOUNDRY_KILL_CAPPING_ENEMY ); + } + } + + if ( InAchievementZone( pTFVictim ) ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "player_killed_achievement_zone" ); + if ( event ) + { + event->SetInt( "attacker", entindex() ); + event->SetInt( "victim", pTFVictim->entindex() ); + gameeventmanager->FireEvent( event ); + } + } + } + } + + // Check for SD_Doomsday achievements + if ( FStrEq( "sd_doomsday", STRING( gpGlobals->mapname ) ) ) + { + if ( pTFVictim && ( pTFVictim->GetTeamNumber() != GetTeamNumber() ) ) + { + // find the flag in the map + CCaptureFlag *pFlag = NULL; + for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i ) + { + pFlag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[i] ); + if ( !pFlag->IsDisabled() ) + { + break; + } + } + + // was the victim in an achievement zone? + CAchievementZone *pZone = InAchievementZone( pTFVictim ); + if ( pZone ) + { + int iZoneID = pZone->GetZoneID(); + if ( iZoneID == 0 ) + { + if ( pFlag && pFlag->IsHome() ) + { + AwardAchievement( ACHIEVEMENT_TF_MAPS_DOOMSDAY_DENY_NEUTRAL_PICKUP ); + } + } + else + { + IGameEvent *event = gameeventmanager->CreateEvent( "player_killed_achievement_zone" ); + if ( event ) + { + event->SetInt( "attacker", entindex() ); + event->SetInt( "victim", pTFVictim->entindex() ); + event->SetInt( "zone_id", iZoneID ); + gameeventmanager->FireEvent( event ); + } + } + } + + // check the flag carrier to see if the victim has recently damaged them + if ( pFlag && pFlag->IsStolen() ) + { + CTFPlayer *pFlagCarrier = ToTFPlayer( pFlag->GetOwnerEntity() ); + if ( pFlagCarrier && ( pFlagCarrier->GetTeamNumber() == GetTeamNumber() ) ) + { + // has the victim damaged the flag carrier in the last 3 seconds? + if ( pFlagCarrier->m_AchievementData.IsDamagerInHistory( pTFVictim, 3.0 ) ) + { + AwardAchievement( ACHIEVEMENT_TF_MAPS_DOOMSDAY_DEFEND_CARRIER ); + } + } + } + } + } + + // Check for CP_Snakewater achievement + if ( FStrEq( "cp_snakewater_final1", STRING( gpGlobals->mapname ) ) ) + { + if ( pTFVictim && ( pTFVictim->GetTeamNumber() != GetTeamNumber() ) ) + { + if ( InAchievementZone( pTFVictim ) ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "player_killed_achievement_zone" ); + if ( event ) + { + event->SetInt( "attacker", entindex() ); + event->SetInt( "victim", pTFVictim->entindex() ); + gameeventmanager->FireEvent( event ); + } + } + } + } + + if ( IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + if ( pVictim->GetTeamNumber() != GetTeamNumber() ) + { + // Check if this kill should refill the charge meter + CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>(info.GetWeapon()); + + float flRefill = 0.0f; + CALL_ATTRIB_HOOK_FLOAT( flRefill, kill_refills_meter ); + if ( m_Shared.GetCarryingRuneType() == RUNE_KNOCKOUT ) // Knockout powerup restricts charge + { + flRefill *= 0.2; + } + + if ( flRefill > 0 && ((info.GetDamageType() & DMG_MELEE) || ( info.GetDamageCustom() == TF_DMG_CUSTOM_CHARGE_IMPACT ) ) ) + { + m_Shared.SetDemomanChargeMeter( m_Shared.GetDemomanChargeMeter() + flRefill * 100.0f ); + } + + if ( ( pWeapon && pWeapon->IsCurrentAttackDuringDemoCharge() ) || ( info.GetDamageCustom() == TF_DMG_CUSTOM_CHARGE_IMPACT ) ) + { + if ( flRefill > 0 ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "kill_refills_meter" ); + if ( event ) + { + event->SetInt( "index", entindex() ); + gameeventmanager->FireEvent( event ); + } + } + + if ( pTFVictim ) + { + // could the attacker see this player when the charge started? + if ( m_Shared.m_hPlayersVisibleAtChargeStart.Find( pTFVictim ) == m_Shared.m_hPlayersVisibleAtChargeStart.InvalidIndex() ) + { + AwardAchievement( ACHIEVEMENT_TF_DEMOMAN_KILL_PLAYER_YOU_DIDNT_SEE ); + } + } + } + + // Demoman achievement: Kill at least 3 players capping or pushing the cart with the same detonation + CTriggerAreaCapture *pAreaTrigger = pTFVictim->GetControlPointStandingOn(); + if ( pAreaTrigger ) + { + CTeamControlPoint *pCP = pAreaTrigger->GetControlPoint(); + if ( pCP ) + { + if ( pCP->GetOwner() == GetTeamNumber() ) + { + if ( GetActiveTFWeapon() && ( GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_PIPEBOMBLAUNCHER ) ) + { + // Add victim to our list + int iIndex = m_Cappers.Find( pTFVictim->GetUserID() ); + if ( iIndex != m_Cappers.InvalidIndex() ) + { + // they're already in our list + m_Cappers[iIndex] = gpGlobals->curtime; + } + else + { + // we need to add them + m_Cappers.Insert( pTFVictim->GetUserID(), gpGlobals->curtime ); + } + // Did we get three? + if ( m_Cappers.Count() >= 3 ) + { + // Traverse the list, comparing the recorded time to curtime + int iHitCount = 0; + FOR_EACH_MAP_FAST ( m_Cappers, cIndex ) + { + // For each match, increment counter + if ( gpGlobals->curtime <= m_Cappers[cIndex] + 0.1f ) + { + iHitCount++; + } + else + { + m_Cappers.Remove( cIndex ); + } + + // If we hit 3, award and purge the group + if ( iHitCount >= 3 ) + { + AwardAchievement( ACHIEVEMENT_TF_DEMOMAN_KILL_X_CAPPING_ONEDET ); + m_Cappers.RemoveAll(); + } + } + } + } + } + // Kill players defending "x" times + else + { + // If we're able to cap the point... + if ( TeamplayGameRules()->TeamMayCapturePoint( GetTeamNumber(), pCP->GetPointIndex() ) && + TeamplayGameRules()->PlayerMayCapturePoint( this, pCP->GetPointIndex() ) ) + { + AwardAchievement( ACHIEVEMENT_TF_DEMOMAN_KILL_X_DEFENDING ); + } + } + } + } + } + } + + // Sniper Kill Rage + if ( IsPlayerClass( TF_CLASS_SNIPER ) ) + { + // Item attribute + // Add Sniper Rage On Kills + float flRageGain = 0; + CALL_ATTRIB_HOOK_FLOAT( flRageGain, rage_on_kill ); + if (flRageGain != 0) + { + m_Shared.ModifyRage(flRageGain); + } + + } + + for ( int i=0; i<m_Shared.m_nNumHealers; i++ ) + { + m_Shared.m_aHealers[i].iKillsWhileBeingHealed++; + if ( IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ) + { + if ( m_Shared.m_aHealers[i].iKillsWhileBeingHealed >= 5 && m_Shared.m_aHealers[i].bDispenserHeal ) + { + // We got five kills while being healed by this dispenser. Reward the engineer with an achievement! + CTFPlayer *pHealScorer = ToTFPlayer( m_Shared.m_aHealers[i].pHealScorer ); + if ( pHealScorer && pHealScorer->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + pHealScorer->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_HEAVY_ASSIST ); + } + } + } + } + + OnKilledOther_Effects( pVictim, info ); + + // track accumulated sentry gun kills on owning player for Sentry Busters in MvM (so they can't clear this by rebuilding their sentry) + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() ); + CTFProjectile_SentryRocket *sentryRocket = dynamic_cast< CTFProjectile_SentryRocket * >( info.GetInflictor() ); + + if ( ( sentry && !sentry->IsDisposableBuilding() ) || sentryRocket ) + { + IncrementSentryGunKillCount(); + } + + // Halloween Death Ghosts + // Check the weapon I used to kill with this player and if it has my desired attribute + if ( TF_IsHolidayActive( kHoliday_HalloweenOrFullMoon ) ) + { + int iHalloweenDeathGhosts = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iHalloweenDeathGhosts, halloween_death_ghosts ); + if ( iHalloweenDeathGhosts > 0 ) + { + if ( pTFVictim->GetTeam()->GetTeamNumber() == TF_TEAM_BLUE ) + { + DispatchParticleEffect( "halloween_player_death_blue", pTFVictim->GetAbsOrigin() + Vector( 0, 0, 32 ), vec3_angle ); + } + else if ( pTFVictim->GetTeam()->GetTeamNumber() == TF_TEAM_RED ) + { + DispatchParticleEffect( "halloween_player_death", pTFVictim->GetAbsOrigin() + Vector( 0, 0, 32 ), vec3_angle ); + } + } + } + + DropDeathCallingCard( this, pTFVictim ); + + if ( pTFVictim != this ) + { + for ( int i=0; i<GetNumWearables(); ++i ) + { + CTFWearableLevelableItem *pItem = dynamic_cast< CTFWearableLevelableItem* >( GetWearable(i) ); + if ( pItem ) + { + pItem->IncrementLevel(); + } + } + } + + if ( pTFVictim ) + { + // was the victim on a control point (includes payload carts) + CTriggerAreaCapture *pAreaTrigger = pTFVictim->GetControlPointStandingOn(); + if ( pAreaTrigger ) + { + CTeamControlPoint *pCP = pAreaTrigger->GetControlPoint(); + if ( pCP && ( pCP->GetOwner() != pTFVictim->GetTeamNumber() ) ) + { + if ( TeamplayGameRules()->TeamMayCapturePoint( pTFVictim->GetTeamNumber(), pCP->GetPointIndex() ) && + TeamplayGameRules()->PlayerMayCapturePoint( pTFVictim, pCP->GetPointIndex() ) ) + { + CTFPlayer *pTFAssister = NULL; + if ( TFGameRules() ) + { + pTFAssister = ToTFPlayer( TFGameRules()->GetAssister( pTFVictim, this, info.GetInflictor() ) ); + } + + IGameEvent *event = gameeventmanager->CreateEvent( "killed_capping_player" ); + if ( event ) + { + event->SetInt( "cp", pCP->GetPointIndex() ); + event->SetInt( "killer", entindex() ); + event->SetInt( "victim", pTFVictim->entindex() ); + event->SetInt( "assister", pTFAssister ? pTFAssister->entindex() : -1 ); + event->SetInt( "priority", 9 ); + + gameeventmanager->FireEvent( event ); + } + } + } + } + } + } + else + { + if ( pVictim->IsBaseObject() ) + { + CBaseObject *pObject = dynamic_cast<CBaseObject *>( pVictim ); + SpeakConceptIfAllowed( MP_CONCEPT_KILLED_OBJECT, pObject->GetResponseRulesModifier() ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Called on kill for primary and second-highest damage dealer +//----------------------------------------------------------------------------- +void CTFPlayer::OnKilledOther_Effects( CBaseEntity *pVictim, const CTakeDamageInfo &info ) +{ + int iHealOnKill = 0; + + if ( IsPlayerClass( TF_CLASS_SPY ) ) + { + int iCloakOnKill = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( GetActiveWeapon(), iCloakOnKill, add_cloak_on_kill ); + if ( iCloakOnKill > 0 ) + { + m_Shared.AddToSpyCloakMeter( iCloakOnKill, true ); + } + } + + CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>( info.GetWeapon() ); + if ( !pWeapon ) + return; + + int iRestoreHealthToPercentageOnKill = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iRestoreHealthToPercentageOnKill, restore_health_on_kill ); + + if ( iRestoreHealthToPercentageOnKill > 0 ) + { + // This attribute should ignore runes + int iRestoreMax = GetMaxHealth() - GetRuneHealthBonus(); + // We add one here to deal with a bizarre problem that comes up leaving you one health short sometimes + // due to bizarre floating point rounding or something equally silly. + int iTargetHealth = ( int )( ( ( float )iRestoreHealthToPercentageOnKill / 100.0f ) * ( float )iRestoreMax ) + 1; + + int iBaseMaxHealth = GetMaxHealth() * 1.5, + iNewHealth = Min( GetHealth() + iTargetHealth, iBaseMaxHealth ), + iDeltaHealth = Max(iNewHealth - GetHealth(), 0); + + TakeHealth( iDeltaHealth, DMG_IGNORE_MAXHEALTH ); + } + + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iHealOnKill, heal_on_kill ); + if ( iHealOnKill != 0 ) + { + int iHealthToAdd = MIN( iHealOnKill, m_Shared.GetMaxBuffedHealth() - m_iHealth ); + TakeHealth( iHealthToAdd, DMG_GENERIC ); + //m_iHealth += iHealthToAdd; + + IGameEvent *event = gameeventmanager->CreateEvent( "player_healonhit" ); + if ( event ) + { + event->SetInt( "amount", iHealthToAdd ); + event->SetInt( "entindex", entindex() ); + item_definition_index_t healingItemDef = INVALID_ITEM_DEF_INDEX; + if ( pWeapon->GetAttributeContainer() && pWeapon->GetAttributeContainer()->GetItem() ) + { + healingItemDef = pWeapon->GetAttributeContainer()->GetItem()->GetItemDefIndex(); + } + event->SetInt( "weapon_def_index", healingItemDef ); + gameeventmanager->FireEvent( event ); + } + } + + int iSpeedBoostOnKill = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iSpeedBoostOnKill, speed_boost_on_kill ); + if ( iSpeedBoostOnKill ) + { + m_Shared.AddCond( TF_COND_SPEED_BOOST, iSpeedBoostOnKill ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::Event_Killed( const CTakeDamageInfo &info ) +{ + CTFPlayer *pPlayerAttacker = NULL; + if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() ) + { + pPlayerAttacker = ToTFPlayer( info.GetAttacker() ); + } + + CTFWeaponBase *pKillerWeapon = NULL; + if ( pPlayerAttacker ) + { + pKillerWeapon = dynamic_cast < CTFWeaponBase * > ( info.GetWeapon() ); + } + + if ( m_Shared.InCond( TF_COND_TAUNTING ) ) + { + static CSchemaItemDefHandle dosidoTaunt( "Square Dance Taunt" ); + static CSchemaItemDefHandle congaTaunt( "Conga Taunt" ); + if ( GetTauntEconItemView() ) + { + if ( GetTauntEconItemView()->GetItemDefinition() == dosidoTaunt ) + { + if ( pKillerWeapon && ( pKillerWeapon->GetTFWpnData().m_iWeaponType == TF_WPN_TYPE_MELEE ) ) + { + if ( pPlayerAttacker ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_TAUNT_DOSIDO_MELLE_KILL ); + } + } + } + else if ( GetTauntEconItemView()->GetItemDefinition() == congaTaunt ) + { + if ( pPlayerAttacker ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "conga_kill" ); + if ( event ) + { + event->SetInt( "index", pPlayerAttacker->entindex() ); + gameeventmanager->FireEvent( event ); + } + } + } + } + + StopTaunt(); + } + + // Cheat this death! + if ( m_Shared.InCond( TF_COND_HALLOWEEN_IN_HELL ) ) + { + // Turn into a ghost + m_Shared.RemoveAllCond(); + m_Shared.AddCond( TF_COND_HALLOWEEN_GHOST_MODE ); + + // Create a puff right where we died to mask the ghost spawning in + DispatchParticleEffect( "ghost_appearation", PATTACH_ABSORIGIN, this ); + + // Check for achievement + if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TRIGGER_HURT ) + { + if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_HIGHTOWER ) ) + { + CTFPlayer *pRecentDamager = TFGameRules()->GetRecentDamager( this, 1, 5.0 ); + if ( pRecentDamager ) + { + pRecentDamager->AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_HELLTOWER_ENVIRONMENTAL_KILLS ); + } + } + } + + CTakeDamageInfo ghostinfo = info; + + // If we were killed by "the world", then give credit to the next damager in the list + CTFPlayer *pRecentDamager = TFGameRules()->GetRecentDamager( this, 1, 10.0 ); + + // If killed by trigger hurt, get last attacker + if ( info.GetAttacker() == info.GetInflictor() && info.GetAttacker() && info.GetAttacker()->IsBSPModel() ) + { + if ( pRecentDamager ) + { + ghostinfo.SetAttacker( pRecentDamager ); + + if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) ) + { + ghostinfo.SetDamageCustom( TF_DMG_CUSTOM_KART ); + pRecentDamager->AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_DOOMSDAY_KILL_KARTS ); + HatAndMiscEconEntities_OnOwnerKillEaterEvent( pRecentDamager, this, kKillEaterEvent_Halloween_UnderworldKills ); + } + } + // if no recent damager, check for HHH + else if ( m_flHHHKartAttackTime > gpGlobals->curtime - 15.0f ) + { + ghostinfo.SetDamageCustom( TF_DMG_CUSTOM_DECAPITATION_BOSS ); + } + } + + if ( pRecentDamager ) + { + // Score the "kill". We don't want any of the other logic, so short circuit here. + pRecentDamager->Event_KilledOther( this, ghostinfo ); + + IGameEvent *pEvent = gameeventmanager->CreateEvent( "kill_in_hell" ); + if ( pEvent ) + { + pEvent->SetInt( "killer", pRecentDamager->GetUserID() ); + pEvent->SetInt( "victim", GetUserID() ); + gameeventmanager->FireEvent( pEvent, true ); + } + } + + FeignDeath( ghostinfo ); + + // Have 1 HP + m_iHealth = 1; + return; + } + + SpeakConceptIfAllowed( MP_CONCEPT_DIED ); + + StateTransition( TF_STATE_DYING ); // Transition into the dying state. + + if ( pPlayerAttacker ) + { + if ( TFGameRules()->IsIT( this ) ) + { + // I was IT - transfer to my killer + TFGameRules()->SetIT( pPlayerAttacker ); + } + + if ( pPlayerAttacker != this ) + { + if ( CTFPlayerDestructionLogic::GetRobotDestructionLogic() && ( CTFPlayerDestructionLogic::GetRobotDestructionLogic()->GetType() == CTFPlayerDestructionLogic::TYPE_PLAYER_DESTRUCTION ) ) + { + // was this the team leader? + if ( CTFPlayerDestructionLogic::GetRobotDestructionLogic()->GetTeamLeader( GetTeamNumber() ) == this ) + { + IGameEvent * event = gameeventmanager->CreateEvent( "team_leader_killed" ); + if ( event ) + { + event->SetInt( "killer", pPlayerAttacker->entindex() ); + event->SetInt( "victim", entindex() ); + gameeventmanager->FireEvent( event ); + } + } + } + } + } + + m_bIsTeleportingUsingEurekaEffect = false; + + for ( int i=0; i<GetNumWearables(); ++i ) + { + CTFWearableLevelableItem *pItem = dynamic_cast< CTFWearableLevelableItem* >( GetWearable(i) ); + if ( pItem ) + { + pItem->ResetLevel(); + } + } + +/* + // We're going to save this for a future date + if ( pPlayerAttacker ) + { + if ( pPlayerAttacker != this ) + { + // Killed by another player + if ( ( TFGameRules()->GetBirthdayPlayer() == this ) || ( TFGameRules()->GetBirthdayPlayer() == NULL ) ) + { + // I was the birthday player (or we don't have one) - transfer to my killer + TFGameRules()->SetBirthdayPlayer( pPlayerAttacker ); + } + } + else + { + // Suicide + if ( TFGameRules()->GetBirthdayPlayer() == this ) + { + // I was the birthday player - reset for suicide + TFGameRules()->SetBirthdayPlayer( NULL ); + } + } + } +*/ + bool bOnGround = GetFlags() & FL_ONGROUND; + bool bElectrocuted = false; + bool bDisguised = m_Shared.InCond( TF_COND_DISGUISED ); + // we want the rag doll to burn if the player was burning and was not a pyro (who only burns momentarily) + bool bBurning = m_Shared.InCond( TF_COND_BURNING ) && ( TF_CLASS_PYRO != GetPlayerClass()->GetClassIndex() ); + CTFPlayer *pOriginalBurner = m_Shared.GetOriginalBurnAttacker(); + CTFPlayer *pLastBurner = m_Shared.GetBurnAttacker(); + + if ( m_aBurnFromBackAttackers.Count() > 0 ) + { + CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>(info.GetWeapon()); + if ( pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_FLAMETHROWER ) + { + for ( int i = 0; i < m_aBurnFromBackAttackers.Count(); i++ ) + { + CTFPlayer *pBurner = ToTFPlayer( m_aBurnFromBackAttackers[i].Get() ); + + if ( pBurner ) + { + pBurner->AwardAchievement( ACHIEVEMENT_TF_PYRO_KILL_FROM_BEHIND ); + } + } + } + + ClearBurnFromBehindAttackers(); + } + + if ( IsPlayerClass( TF_CLASS_MEDIC ) ) + { + CWeaponMedigun* pMedigun = assert_cast<CWeaponMedigun*>( Weapon_OwnsThisID( TF_WEAPON_MEDIGUN ) ); + float flChargeLevel = pMedigun ? pMedigun->GetChargeLevel() : 0.f; + float flMinChargeLevel = pMedigun ? pMedigun->GetMinChargeAmount() : 1.f; + + bool bCharged = flChargeLevel >= flMinChargeLevel; + + if ( bCharged ) + { + // Had an ubercharge ready at death? + CEconEntity *pVictimEconWeapon = dynamic_cast<CEconEntity *>( GetActiveTFWeapon() ); + EconEntity_OnOwnerKillEaterEventNoPartner( pVictimEconWeapon, this, kKillEaterEvent_NEGATIVE_UbersDropped ); + + bElectrocuted = true; + if ( pPlayerAttacker ) + { + if ( pPlayerAttacker->IsPlayerClass( TF_CLASS_SCOUT ) ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SCOUT_KILL_CHARGED_MEDICS ); + } + else if ( pPlayerAttacker->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SNIPER_KILL_CHARGED_MEDIC ); + } + else if ( pPlayerAttacker->IsPlayerClass( TF_CLASS_SPY ) ) + { + if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BACKSTAB ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SPY_BACKSTAB_MEDIC_CHARGED ); + } + } + + CTF_GameStats.Event_PlayerAwardBonusPoints( pPlayerAttacker, this, 20 ); + } + } + + // Disable radius healing + m_Shared.Heal_Radius( false ); + + IGameEvent * event = gameeventmanager->CreateEvent( "medic_death" ); + if ( event ) + { + int iHealing = 0; + + PlayerStats_t *pPlayerStats = CTF_GameStats.FindPlayerStats( this ); + if ( pPlayerStats ) + { + iHealing = pPlayerStats->statsCurrentLife.m_iStat[TFSTAT_HEALING]; + + // defensive fix for the moment for bug where healing value becomes bogus sometimes: if bogus, slam it to 0 + // ...copied from CTFGameRules::CalcPlayerScore() + if ( iHealing < 0 || iHealing > 10000000 ) + { + iHealing = 0; + } + } + + event->SetInt( "userid", GetUserID() ); + event->SetInt( "attacker", pPlayerAttacker ? pPlayerAttacker->GetUserID() : 0 ); + event->SetInt( "healing", iHealing ); + event->SetBool( "charged", bCharged ); + + gameeventmanager->FireEvent( event ); + } + } + else if ( IsPlayerClass( TF_CLASS_SOLDIER ) || IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + if ( pPlayerAttacker && pPlayerAttacker->IsPlayerClass( TF_CLASS_SNIPER ) && RocketJumped() && !GetGroundEntity() ) + { + if ( pKillerWeapon ) + { + if ( WeaponID_IsSniperRifleOrBow( pKillerWeapon->GetWeaponID() ) ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SNIPER_KILL_RJER ); + } + + if ( pKillerWeapon->GetWeaponID() == TF_WEAPON_SNIPERRIFLE_CLASSIC ) + { + if ( ( info.GetDamageCustom() == TF_DMG_CUSTOM_HEADSHOT ) && ( info.GetDamageType() & DMG_CRITICAL ) ) + { + if ( pPlayerAttacker->m_Shared.IsAiming() == false ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SNIPER_CLASSIC_RIFLE_HEADSHOT_JUMPER ); + } + } + } + } + } + } + else if ( IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + if ( pPlayerAttacker && pPlayerAttacker->IsPlayerClass( TF_CLASS_SOLDIER ) ) + { + // Has Engineer worked on his sentrygun recently? + CBaseObject *pSentry = GetObjectOfType( OBJ_SENTRYGUN ); + if ( pSentry && m_AchievementData.IsTargetInHistory( pSentry, 4.0 ) ) + { + if ( pSentry->m_AchievementData.CountDamagersWithinTime( 3.0 ) > 0 ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SOLDIER_KILL_ENGY ); + } + } + } + + if ( m_Shared.IsCarryingObject() ) + { + CTakeDamageInfo info( pPlayerAttacker, pPlayerAttacker, NULL, vec3_origin, GetAbsOrigin(), 0, DMG_GENERIC ); + info.SetDamageCustom( TF_DMG_CUSTOM_CARRIED_BUILDING ); + if ( m_Shared.GetCarriedObject() != NULL ) + { + m_Shared.GetCarriedObject()->Killed( info ); + + // Killeater event for being killed while carrying a building + CEconEntity *pVictimEconWeapon = dynamic_cast<CEconEntity *>( Weapon_OwnsThisID( TF_WEAPON_WRENCH ) ); + EconEntity_OnOwnerKillEaterEventNoPartner( pVictimEconWeapon, this, kKillEaterEvent_NEGATIVE_DeathsWhileCarryingBuilding ); + } + } + } + else if ( IsPlayerClass( TF_CLASS_SNIPER ) ) + { + if ( pPlayerAttacker ) + { + if ( GetActiveTFWeapon() && ( GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_SNIPERRIFLE_CLASSIC ) ) + { + if ( pKillerWeapon && ( pKillerWeapon->GetTFWpnData().m_iWeaponType == TF_WPN_TYPE_MELEE ) ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_MELEE_KILL_CLASSIC_RIFLE_SNIPER ); + } + } + } + } + + if ( pPlayerAttacker ) + { + if ( pPlayerAttacker->IsPlayerClass( TF_CLASS_SOLDIER ) ) + { + if ( pPlayerAttacker->RocketJumped() || (gpGlobals->curtime - pPlayerAttacker->m_flBlastJumpLandTime) < 1 ) + { + if ( pKillerWeapon && pKillerWeapon->GetWeaponID() == TF_WEAPON_SHOVEL ) + { + CTFShovel *pShovel = static_cast< CTFShovel* >( pKillerWeapon ); + if ( pShovel && pShovel->GetShovelType() == SHOVEL_DAMAGE_BOOST ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SOLDIER_RJ_EQUALIZER_KILL ); + } + } + } + } + else if ( pPlayerAttacker->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + if ( pKillerWeapon && WeaponID_IsSniperRifle( pKillerWeapon->GetWeaponID() ) && pPlayerAttacker->m_Shared.IsAiming() == false ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SNIPER_KILL_UNSCOPED ); + } + + if ( pKillerWeapon && ( pKillerWeapon->GetWeaponID() == TF_WEAPON_SNIPERRIFLE_CLASSIC ) ) + { + if ( ( info.GetDamageCustom() == TF_DMG_CUSTOM_HEADSHOT ) && ( info.GetDamageType() & DMG_CRITICAL ) ) + { + if ( pPlayerAttacker->m_Shared.IsAiming() == false ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SNIPER_CLASSIC_RIFLE_NOSCOPE_HEADSHOT ); + } + } + } + } + else if ( pPlayerAttacker->IsPlayerClass( TF_CLASS_SPY ) ) + { +#ifdef STAGING_ONLY + // Move to Killed Other + // Spy Tranq Buff + if ( m_Shared.InCond( TF_COND_TRANQ_MARKED ) && info.GetDamageCustom() == TF_DMG_CUSTOM_BACKSTAB ) + { + int iTranq = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pPlayerAttacker, iTranq, override_projectile_type ); + if ( iTranq == TF_PROJECTILE_TRANQ ) + { + // BIGGEST HACK EVER + int iDesiredClass = GetPlayerClass()->GetClassIndex(); + + if ( iDesiredClass != TF_CLASS_SPY ) + { + pPlayerAttacker->GetPlayerClass()->Init( iDesiredClass ); + + for ( int i = 0; i < MAX_WEAPONS; i++ ) + { + CTFWeaponBase *pWeapon = (CTFWeaponBase *)pPlayerAttacker->GetWeapon( i ); + if ( pWeapon ) + { + pWeapon->OnOwnerClassChange(); + } + } + + pPlayerAttacker->RemoveAllItems( true ); + + // TODO: move this into conditions + pPlayerAttacker->RemoveTeleportEffect(); + + // remove invisibility very quickly + pPlayerAttacker->m_Shared.FadeInvis( 0.1f ); + + // Stop any firing that was taking place before respawn. + pPlayerAttacker->m_nButtons = 0; + + // Possibly Save and set their health percentage here + Vector vAttackerPos = pPlayerAttacker->GetAbsOrigin(); + QAngle qAttackerAngle = pPlayerAttacker->GetAbsAngles(); + + pPlayerAttacker->StateTransition( TF_STATE_ACTIVE ); + pPlayerAttacker->Spawn(); + + pPlayerAttacker->Teleport( &vAttackerPos, &qAttackerAngle, &vec3_origin ); + + pPlayerAttacker->m_Shared.AddCond( TF_COND_SPY_CLASS_STEAL ); + + // Overheal + pPlayerAttacker->SetHealth( pPlayerAttacker->GetMaxHealth() * 1.5f ); + + // Steal their uber + if ( IsPlayerClass( TF_CLASS_MEDIC ) ) + { + // Steal Enemy Uber + CWeaponMedigun *pMedigun = (CWeaponMedigun *)Weapon_OwnsThisID( TF_WEAPON_MEDIGUN ); + if ( pMedigun ) + { + float flCharge = pMedigun->GetChargeLevel(); + CWeaponMedigun *pAttackerMedigun = (CWeaponMedigun *)pPlayerAttacker->Weapon_OwnsThisID( TF_WEAPON_MEDIGUN ); + if ( pAttackerMedigun ) + { + pAttackerMedigun->AddCharge( flCharge ); + } + } + } + + // Steal Rage + pPlayerAttacker->m_Shared.SetRageMeter( m_Shared.GetRageMeter() ); + + // Steal heads + pPlayerAttacker->m_Shared.SetDecapitations( m_Shared.GetDecapitations() ); + + // Effects + //pPlayerAttacker->EmitSound( "Player.Spy_Disguise" ); + pPlayerAttacker->EmitSound( "WeaponDNAGun.Transform" ); + Vector vOrigin = pPlayerAttacker->GetAbsOrigin(); + CPVSFilter filter( vOrigin ); + + switch ( pPlayerAttacker->GetTeamNumber() ) + { + case TF_TEAM_RED: + TE_TFParticleEffect( filter, 0.0, "teleported_red", vOrigin, vec3_angle ); + TE_TFParticleEffect( filter, 0.0, "player_sparkles_red", vOrigin, vec3_angle, pPlayerAttacker, PATTACH_POINT ); + break; + case TF_TEAM_BLUE: + TE_TFParticleEffect( filter, 0.0, "teleported_blue", vOrigin, vec3_angle ); + TE_TFParticleEffect( filter, 0.0, "player_sparkles_blue", vOrigin, vec3_angle, pPlayerAttacker, PATTACH_POINT ); + break; + default: + break; + } + } + } + } +#endif // STAGING_ONLY + CTriggerAreaCapture *pAreaTrigger = GetControlPointStandingOn(); + if ( pAreaTrigger ) + { + CTeamControlPoint *pCP = pAreaTrigger->GetControlPoint(); + if ( pCP ) + { + if ( pCP->GetOwner() == GetTeamNumber() ) + { + // killed on a control point owned by my team + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SPY_KILL_CP_DEFENDERS ); + } + else + { + // killed on a control point NOT owned by my team, was it a backstab? + if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BACKSTAB ) + { + // was i able to capture the control point? + if ( TeamplayGameRules()->TeamMayCapturePoint( GetTeamNumber(), pCP->GetPointIndex() ) && + TeamplayGameRules()->PlayerMayCapturePoint( this, pCP->GetPointIndex() ) ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SPY_BACKSTAB_CAPPING_ENEMIES ); + } + } + } + } + } + + if ( IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + //m_AchievementData.CountTargetsWithinTime + int iHistory = 0; + EntityHistory_t *pHistory = m_AchievementData.GetTargetHistory( iHistory ); + + while ( pHistory ) + { + if ( pHistory->hEntity && pHistory->hEntity->IsBaseObject() && m_AchievementData.IsTargetInHistory( pHistory->hEntity, 1.0f ) ) + { + CBaseObject *pObject = dynamic_cast<CBaseObject *>( pHistory->hEntity.Get() ); + + if ( pObject->ObjectType() == OBJ_SENTRYGUN ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SPY_KILL_WORKING_ENGY ); + break; + } + } + + iHistory++; + pHistory = m_AchievementData.GetTargetHistory( iHistory ); + } + } + } + else if ( pPlayerAttacker->IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + // Kill "x" players with a direct pipebomb hit + if ( pPlayerAttacker->GetActiveTFWeapon() && ( pPlayerAttacker->GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_GRENADELAUNCHER ) ) + { + CBaseEntity *pInflictor = info.GetInflictor(); + + if ( pInflictor && pInflictor->IsPlayer() == false ) + { + CTFGrenadePipebombProjectile *pBaseGrenade = dynamic_cast< CTFGrenadePipebombProjectile* >( pInflictor ); + + if ( pBaseGrenade && pBaseGrenade->m_bTouched != true ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_DEMOMAN_KILL_X_WITH_DIRECTPIPE ); + } + } + } + } + else if ( pPlayerAttacker->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + // give achievement for killing someone who was recently damaged by our sentry + // note that we don't check to see if the sentry is still alive + if ( pKillerWeapon && + ( pKillerWeapon->GetWeaponID() == TF_WEAPON_SENTRY_REVENGE || + pKillerWeapon->GetWeaponID() == TF_WEAPON_SHOTGUN_PRIMARY ) ) + { + if ( m_AchievementData.IsSentryDamagerInHistory( pPlayerAttacker, 5.0 ) ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_SHOTGUN_KILL_PREV_SENTRY_TARGET ); + } + } + } + + // Revenge Crits for Diamondback + if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BACKSTAB ) + { + pPlayerAttacker->m_Shared.IncrementRevengeCrits(); + } + } + + // Check for CP_Foundry achievement + if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TRIGGER_HURT ) + { + if ( FStrEq( "cp_foundry", STRING( gpGlobals->mapname ) ) ) + { + CTFPlayer *pRecentDamager = TFGameRules()->GetRecentDamager( this, 1, 5.0 ); + if ( pRecentDamager ) + { + pRecentDamager->AwardAchievement( ACHIEVEMENT_TF_MAPS_FOUNDRY_PUSH_INTO_CAULDRON ); + } + } + } + + // Record if we were stunned for achievement tracking. + m_iOldStunFlags = m_Shared.GetStunFlags(); + + // Determine the optional assist for the kill. + DetermineAssistForKill( info ); + + // put here to stop looping kritz sound from playing til respawn. + if ( m_Shared.InCond( TF_COND_CRITBOOSTED ) ) + { + StopSound( "TFPlayer.CritBoostOn" ); + } + + if ( m_Shared.InCond( TF_COND_HALLOWEEN_BOMB_HEAD ) ) + { + SetPendingMerasmusPlayerBombExplode(); + } + + // check for MvM achievements + if ( TFGameRules()->IsMannVsMachineMode() && IsBot() ) + { + if ( pPlayerAttacker && ( pPlayerAttacker->GetTeamNumber() == TF_TEAM_PVE_DEFENDERS ) ) + { + if ( FStrEq( "mvm_mannhattan", STRING( gpGlobals->mapname ) ) ) + { + CTFBot *pBot = dynamic_cast< CTFBot* >( this ); + if ( pBot ) + { + // kill gate bots + if ( pBot->HasTag( "bot_gatebot" ) ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_MVM_MAPS_MANNHATTAN_BOMB_BOT_GRIND ); + } + } + + // kill stunned bots + if ( m_Shared.InCond( TF_COND_MVM_BOT_STUN_RADIOWAVE ) ) + { + if ( g_pPopulationManager->IsAdvancedPopFile() ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_adv_wave_killed_stun_radio" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + } + } + } + } + } + + // Reset our model if we were disguised + if ( bDisguised ) + { + UpdateModel(); + } + + RemoveTeleportEffect(); + + // Drop a pack with their leftover ammo + // Arena: Only do this if the match hasn't started yet. + if ( ShouldDropAmmoPack() ) + { + DropAmmoPack( info, false, false ); + } + + if ( TFGameRules()->IsInMedievalMode() ) + { + DropHealthPack( info, true ); + } + +#ifdef TF_RAID_MODE + // Bots sometimes drop health kits in Raid Mode + if ( TFGameRules()->IsRaidMode() && GetTeamNumber() == TF_TEAM_RED ) + { + if ( RandomInt( 1, 100 ) <= tf_raid_drop_healthkit_chance.GetInt() ) + { + DropHealthPack( info, true ); + } + } +#endif // TF_RAID_MODE + + // PvE mode credits/currency + if ( TFGameRules()->IsMannVsMachineMode() ) + { + MannVsMachineStats_PlayerEvent_Died( this ); + + if ( IsBot() ) + { + m_nCurrency = 0; + if ( !IsMissionEnemy() && m_pWaveSpawnPopulator ) + { + m_nCurrency = m_pWaveSpawnPopulator->GetCurrencyAmountPerDeath(); + } + + // only drop currency if the map designer has specified it + if ( m_nCurrency > 0 ) + { + // We only drop a pack when the game's accumulated enough to make it worth it + int nDropAmount = TFGameRules()->CalculateCurrencyAmount_CustomPack( m_nCurrency ); + if ( nDropAmount ) + { + bool bDropPack = true; + + // Give money directly to the team if a trigger killed us + if ( info.GetDamageType() ) + { + CBaseTrigger *pTrigger = dynamic_cast< CBaseTrigger *>( info.GetInflictor() ); + if ( pTrigger ) + { + bDropPack = false; + TFGameRules()->DistributeCurrencyAmount( nDropAmount, NULL, true, true ); + } + } + + if ( bDropPack ) + { + CTFPlayer* pMoneyMaker = NULL; + if ( pPlayerAttacker && pPlayerAttacker->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BLEEDING || ( pKillerWeapon && WeaponID_IsSniperRifleOrBow( pKillerWeapon->GetWeaponID() ) ) ) + { + pMoneyMaker = pPlayerAttacker; + + if ( IsHeadshot( info.GetDamageCustom() ) || ( LastHitGroup() == HITGROUP_HEAD && pKillerWeapon && pKillerWeapon->GetJarateTime() ) ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_sniper_headshot_currency" ); + if ( event ) + { + event->SetInt( "userid", pPlayerAttacker->GetUserID() ); + event->SetInt( "currency", nDropAmount ); + gameeventmanager->FireEvent( event ); + } + } + } + } + + int iForceDistributeCurrency = 0; + CALL_ATTRIB_HOOK_INT( iForceDistributeCurrency, force_distribute_currency_on_death ); + bool bForceDistribute = iForceDistributeCurrency != 0; + + // if I'm force to distribute currency, just give the credit to the attacker + if ( !pMoneyMaker && bForceDistribute ) + { + pMoneyMaker = pPlayerAttacker; + } + + DropCurrencyPack( TF_CURRENCY_PACK_CUSTOM, nDropAmount, bForceDistribute, pMoneyMaker ); + } + } + } + + if ( !m_bIsSupportEnemy ) + { + unsigned int iFlags = m_bIsMissionEnemy ? MVM_CLASS_FLAG_MISSION : MVM_CLASS_FLAG_NORMAL; + if ( IsMiniBoss() ) + { + iFlags |= MVM_CLASS_FLAG_MINIBOSS; + } + + TFObjectiveResource()->DecrementMannVsMachineWaveClassCount( GetPlayerClass()->GetClassIconName(), iFlags ); + } + + if ( m_bIsLimitedSupportEnemy ) + { + TFObjectiveResource()->DecrementMannVsMachineWaveClassCount( GetPlayerClass()->GetClassIconName(), MVM_CLASS_FLAG_SUPPORT_LIMITED ); + } + + // Electrical effect whenever a bot dies + CPVSFilter filter( WorldSpaceCenter() ); + TE_TFParticleEffect( filter, 0.f, "bot_death", GetAbsOrigin(), vec3_angle ); + } + else + { + // Players lose money for dying + RemoveCurrency( tf_mvm_death_penalty.GetInt() ); + } + + // tell the population manager a player died + // THIS MUST HAPPEN AFTER THE CURRENCY CALCULATION (ABOVE) + // NOW THAT WE'RE CALCULATING CURRENCY ON-DEATH INSTEAD OF ON-SPAWN + if ( g_pPopulationManager ) + { + g_pPopulationManager->OnPlayerKilled( this ); + } + + if ( IsBot() && HasTheFlag() && GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + int nLevel = TFObjectiveResource()->GetFlagCarrierUpgradeLevel(); + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_bomb_carrier_killed" ); + if ( event ) + { + event->SetInt( "level", nLevel ); + gameeventmanager->FireEvent( event ); + } + } + + if ( !IsBot() && !m_hReviveMarker ) + { + m_hReviveMarker = CTFReviveMarker::Create( this ); + } + } + + // This system is designed to coarsely measure a player's skill in public pvp games. +// UpdateSkillRatingData(); + +#ifdef STAGING_ONLY + if ( TFGameRules()->IsBountyMode() ) + { + // Lose unspent currency on death? + float flPenalty = tf_bountymode_currency_penalty_ondeath.GetFloat(); + if ( flPenalty ) + { + int nAmount = GetCurrency(); + if ( nAmount ) + { + nAmount *= ( 1.f - flPenalty ); + SetCurrency( Max( nAmount, 0 ) ); + } + } + + if ( tf_bountymode_upgrades_wipeondeath.GetInt() ) + { + // Remove upgrade attributes from the player and their items + if ( g_hUpgradeEntity ) + { + g_hUpgradeEntity->GrantOrRemoveAllUpgrades( this, true, false ); + } + + // Remove the appropriate upgrade info from upgrade histories + if ( g_pPopulationManager ) + { + g_pPopulationManager->RemovePlayerAndItemUpgradesFromHistory( this ); + } + } + } +#endif // STAGING_ONLY + + if ( pPlayerAttacker ) + { + int iDropHealthOnKill = 0; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pPlayerAttacker, iDropHealthOnKill, drop_health_pack_on_kill ); + if ( iDropHealthOnKill == 1 ) + { + DropHealthPack( info, true ); + } + + int iKillForcesAttackerToLaugh = 0; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pPlayerAttacker, iKillForcesAttackerToLaugh, kill_forces_attacker_to_laugh ); + if ( iKillForcesAttackerToLaugh == 1 ) + { + // force yourself to laugh! + pPlayerAttacker->Taunt( TAUNT_MISC_ITEM, MP_CONCEPT_TAUNT_LAUGH ); + } + } + + // If the player has a capture flag and was killed by another player, award that player a defense + if ( HasItem() && pPlayerAttacker && ( pPlayerAttacker != this ) ) + { + CCaptureFlag *pCaptureFlag = dynamic_cast<CCaptureFlag *>( GetItem() ); + if ( pCaptureFlag ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_flag_event" ); + if ( event ) + { + event->SetInt( "player", pPlayerAttacker->entindex() ); + event->SetInt( "eventtype", TF_FLAGEVENT_DEFEND ); + event->SetInt( "carrier", entindex() ); + event->SetInt( "priority", 8 ); + event->SetInt( "team", pCaptureFlag->GetTeamNumber() ); + gameeventmanager->FireEvent( event ); + } + CTF_GameStats.Event_PlayerDefendedPoint( pPlayerAttacker ); + + if ( !CTFPlayerDestructionLogic::GetRobotDestructionLogic() || ( CTFPlayerDestructionLogic::GetRobotDestructionLogic()->GetType() != CTFPlayerDestructionLogic::TYPE_PLAYER_DESTRUCTION ) ) + { + if ( pPlayerAttacker && pPlayerAttacker->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + CTFWeaponBase *pKillerWeapon = dynamic_cast < CTFWeaponBase * > ( info.GetWeapon() ); + + if ( pKillerWeapon && pKillerWeapon->GetWeaponID() == TF_WEAPON_COMPOUND_BOW ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_SNIPER_BOW_KILL_FLAGCARRIER ); + } + } + + // Handle the "you killed someone with the flag" event. We can't handle this with the usual block + // in PlayerKilled() because by that point we've forgotten that we had the flag. + EconEntity_OnOwnerKillEaterEvent( dynamic_cast<CEconEntity *>( pKillerWeapon ), pPlayerAttacker, this, kKillEaterEvent_DefenderKill ); + } + } + } + + CTFWeaponBase* pActiveWeapon = GetActiveTFWeapon(); + if( pActiveWeapon ) + { + CEconEntity *pVictimEconWeapon = dynamic_cast<CEconEntity *>( pActiveWeapon ); + + EconEntity_OnOwnerKillEaterEventNoPartner( pVictimEconWeapon, this, kKillEaterEvent_NEGATIVE_Deaths ); + + // Check if we died from environmental damage + CBaseTrigger *pTrigger = dynamic_cast< CBaseTrigger *>( info.GetInflictor() ); + if ( pTrigger ) + { + EconEntity_OnOwnerKillEaterEventNoPartner( pVictimEconWeapon, this, kKillEaterEvent_NEGATIVE_DeathsFromEnvironment ); + } + + // Check if we died from fall damage + if( info.GetDamageType() == DMG_FALL ) + { + EconEntity_OnOwnerKillEaterEventNoPartner( pVictimEconWeapon, this, kKillEaterEvent_NEGATIVE_DeathsFromCratering ); + } + } + + 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 this was a rocket/grenade kill that didn't gib, exaggerated the blast force + if ( ( info.GetDamageType() & DMG_BLAST ) != 0 ) + { + Vector vForceModifier = info.GetDamageForce(); + vForceModifier.x *= 2.5; + vForceModifier.y *= 2.5; + vForceModifier.z *= 2; + info_modified.SetDamageForce( vForceModifier ); + } + } + + if ( bElectrocuted && bGib ) + { + const char *pEffectName = ( GetTeamNumber() == TF_TEAM_RED ) ? "electrocuted_gibbed_red" : "electrocuted_gibbed_blue"; + DispatchParticleEffect( pEffectName, GetAbsOrigin(), vec3_angle ); + EmitSound( "TFPlayer.MedicChargedDeath" ); + } + + SetGibbedOnLastDeath( bGib ); + + bool bIsMvMRobot = TFGameRules()->IsMannVsMachineMode() && IsBot(); + if ( bGib && !bIsMvMRobot && IsPlayerClass( TF_CLASS_SCOUT ) && RandomInt( 1, 100 ) <= SCOUT_ADD_BIRD_ON_GIB_CHANCE ) + { + Vector vecPos = WorldSpaceCenter(); + SpawnClientsideFlyingBird( vecPos ); + } + + // 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 + if ( m_Shared.InCond( TF_COND_HALLOWEEN_IN_HELL ) ) + { + m_hObserverTarget.Set( pPlayerAttacker ); + } + else + { + 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 if ( info.GetAttacker() && TFGameRules()->GetActiveBoss() && info.GetAttacker()->entindex() == TFGameRules()->GetActiveBoss()->entindex() ) + { + // killed by the boss - look at him + m_hObserverTarget.Set( info.GetAttacker() ); + } + else + { + m_hObserverTarget.Set( NULL ); + } + + bool bSuicide = false; + if ( info_modified.GetDamageCustom() == TF_DMG_CUSTOM_SUICIDE ) + { + bSuicide = true; + // 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 ) ); + } + else if ( info.GetAttacker() == this ) + { + bSuicide = true; + // If we killed ourselves in non-suicide fashion, and we've been hurt lately, give that guy the kill. + CTFPlayer *pRecentDamager = TFGameRules()->GetRecentDamager( this, 0, 5.0 ); + if ( pRecentDamager ) + { + info_modified.SetDamageCustom( TF_DMG_CUSTOM_SUICIDE ); + info_modified.SetDamageType( DMG_GENERIC ); + info_modified.SetAttacker( pRecentDamager ); + info_modified.SetWeapon( NULL ); + info_modified.SetInflictor( NULL ); + } + } + else if ( info.GetAttacker() == info.GetInflictor() && info.GetAttacker() && info.GetAttacker()->IsBSPModel() ) + { + bSuicide = true; + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + // If we were killed by "the world", then give credit to the next damager in the list + CTFPlayer *pRecentDamager = TFGameRules()->GetRecentDamager( this, 1, 10.0 ); + if ( pRecentDamager ) + { + //info_modified.SetDamageCustom( TF_DMG_CUSTOM_SUICIDE ); + info_modified.SetDamageType( DMG_GENERIC ); + info_modified.SetAttacker( pRecentDamager ); + info_modified.SetWeapon( NULL ); + info_modified.SetInflictor( NULL ); + } + } + else + { + // If we were killed by "the world", then give credit to the next damager in the list + CTFPlayer *pRecentDamager = TFGameRules()->GetRecentDamager( this, 1, 5.0 ); + if ( pRecentDamager ) + { + info_modified.SetDamageCustom( TF_DMG_CUSTOM_SUICIDE ); + info_modified.SetDamageType( DMG_GENERIC ); + info_modified.SetAttacker( pRecentDamager ); + info_modified.SetWeapon( NULL ); + info_modified.SetInflictor( NULL ); + } + else if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) && ( info_modified.GetDamageType() & DMG_CLUB ) ) + { + info_modified.SetDamageCustom( TF_DMG_CUSTOM_GIANT_HAMMER ); + info_modified.SetDamageType( info_modified.GetDamageType() | DMG_CRITICAL ); + } + } + } + + if ( pPlayerAttacker && pPlayerAttacker->m_Shared.InCond( TF_COND_HALLOWEEN_TINY ) && !pPlayerAttacker->m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + { + info_modified.SetDamageCustom( TF_DMG_CUSTOM_SPELL_TINY ); + if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_DOOMSDAY_TINY_SMASHER ); + } + } + + if ( TFGameRules() && TFGameRules()->IsPowerupMode() ) + { + // Report Kill + CTF_GameStats.Event_PowerUpModeDeath( pPlayerAttacker, this ); + } + + // Drop your powerup rune when you die + if ( m_Shared.IsCarryingRune() ) + { + int iTeam = GetEnemyTeam( GetTeamNumber() ); // Dead players drop opposing team colored powerups + CTFRune::CreateRune( GetAbsOrigin(), m_Shared.GetCarryingRuneType(), iTeam, true, false ); + } + + // in PD, player death adds points to the flag drop + if ( CTFPlayerDestructionLogic::GetRobotDestructionLogic() + && CTFPlayerDestructionLogic::GetRobotDestructionLogic()->GetType() == CTFPlayerDestructionLogic::TYPE_PLAYER_DESTRUCTION ) + { + CTFPlayer *pRecentDamager = TFGameRules()->GetRecentDamager( this, 0, 5.0 ); + int pointsOnDeath = ( !bSuicide || pRecentDamager ) ? CTFPlayerDestructionLogic::GetPlayerDestructionLogic()->GetPointsOnPlayerDeath() : 0; + + CCaptureFlag *pFlag = NULL; + if ( HasItem() ) + { + pFlag = dynamic_cast<CCaptureFlag*>( GetItem() ); + } + else + { + if ( pointsOnDeath && !PointInRespawnRoom( this, WorldSpaceCenter() ) ) + { + pFlag = CCaptureFlag::Create( GetAbsOrigin(), CTFPlayerDestructionLogic::GetPlayerDestructionLogic()->GetPropModelName(), TF_FLAGTYPE_PLAYER_DESTRUCTION ); + } + } + + if ( pFlag ) + { + // don't add more point to the dropping flag if the player suicided + if ( pointsOnDeath ) + { + pFlag->AddPointValue( pointsOnDeath ); + } + pFlag->Drop( this, true, true, true ); + } + } + + CTFPlayerResource *pResource = dynamic_cast<CTFPlayerResource *>( g_pPlayerResource ); + if ( pResource ) + { + pResource->SetPlayerClassWhenKilled( entindex(), GetPlayerClass()->GetClassIndex() ); + } + + BaseClass::Event_Killed( info_modified ); + + if ( !m_bSwitchedClass ) + { + SaveLastWeaponSlot(); + } + // 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() ) + { + m_iActiveWeaponTypePriorToDeath = GetActiveTFWeapon()->GetWeaponID(); + if ( m_iActiveWeaponTypePriorToDeath == TF_WEAPON_BUILDER ) + m_iActiveWeaponTypePriorToDeath = 0; + GetActiveWeapon()->SendViewModelAnim( ACT_IDLE ); + GetActiveWeapon()->Holster(); + SetActiveWeapon( NULL ); + } + else + { + m_iActiveWeaponTypePriorToDeath = 0; + } + + int iIceRagdoll = 0; + + CTFPlayer *pInflictor = ToTFPlayer( info.GetInflictor() ); + if ( ( IsHeadshot( info.GetDamageCustom() ) ) && pPlayerAttacker ) + { + CTFWeaponBase *pWpn = ( CTFWeaponBase *) info.GetWeapon(); + bool bBowShot = false; + if ( pWpn && pWpn->GetWeaponID() == TF_WEAPON_COMPOUND_BOW ) + { + bBowShot = true; + } + CTF_GameStats.Event_Headshot( pPlayerAttacker, bBowShot ); + } + else if ( ( TF_DMG_CUSTOM_BACKSTAB == info.GetDamageCustom() ) && pInflictor ) + { + CTF_GameStats.Event_Backstab( pInflictor ); + + if ( pKillerWeapon ) + { + CALL_ATTRIB_HOOK_INT_ON_OTHER( pKillerWeapon, iIceRagdoll, freeze_backstab_victim ); + } + } + + bool bCloakedCorpse = false; + if ( pKillerWeapon && pKillerWeapon->GetWeaponID() == TF_WEAPON_KNIFE ) + { + CTFKnife *pKnife = dynamic_cast<CTFKnife*>( pKillerWeapon ); + if ( pKnife && pKnife->ShouldDisguiseOnBackstab() ) + { + bCloakedCorpse = true; + } + } + + int iGoldRagdoll = 0; + if ( pKillerWeapon ) + { + CALL_ATTRIB_HOOK_INT_ON_OTHER( pKillerWeapon, iGoldRagdoll, set_turn_to_gold ); + } + + int iRagdollsBecomeAsh = 0; + if ( info.GetWeapon() ) + { + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iRagdollsBecomeAsh, ragdolls_become_ash ); + } + + int iRagdollsPlasmaEffect = 0; + if ( info.GetWeapon() ) + { + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iRagdollsPlasmaEffect, ragdolls_plasma_effect ); + } + + int iCustomDamage = info.GetDamageCustom(); + if ( iRagdollsPlasmaEffect ) + { + iCustomDamage = TF_DMG_CUSTOM_PLASMA; + } + + int iCritOnHardHit = 0; + if ( info.GetWeapon() ) + { + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iCritOnHardHit, crit_on_hard_hit ); + } + + // Create the ragdoll entity. + if ( bGib || bRagdoll ) + { + CreateRagdollEntity( bGib, bBurning, bElectrocuted, bOnGround, bCloakedCorpse, iGoldRagdoll != 0, iIceRagdoll != 0, iRagdollsBecomeAsh != 0, iCustomDamage, ( iCritOnHardHit != 0 ) ); + } + +#ifdef STAGING_ONLY + // Spy Mark removal on others when killed + // Only check if I have the spy marking gun + // STAGING_SPY + int iTranq = 0; + CALL_ATTRIB_HOOK_INT( iTranq, override_projectile_type ); + if ( iTranq == TF_PROJECTILE_TRANQ ) + { + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, GetTeamNumber() == TF_TEAM_BLUE ? TF_TEAM_RED : TF_TEAM_BLUE, true ); + FOR_EACH_VEC ( playerVector, i ) + { + if ( playerVector[i]->m_Shared.GetConditionProvider( TF_COND_TRANQ_MARKED ) == this ) + { + playerVector[i]->m_Shared.RemoveCond( TF_COND_TRANQ_MARKED ); + } + } + } + + // If I was a spy cloned, give me instant respawn + if ( m_Shared.InCond( TF_COND_SPY_CLASS_STEAL ) ) + { + m_flRespawnTimeOverride = 2.0f; + } + +#endif + + // Remove all conditions... + m_Shared.RemoveAllCond(); + + // 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() && TFGameRules()->IsInArenaMode() == false ) + { + for (int i = GetObjectCount()-1; i >= 0; i--) + { + CBaseObject *obj = GetObject(i); + Assert( obj ); + + if ( obj ) + { + obj->DetonateObject(); + } + } + } + + // Achievement checks + if ( pPlayerAttacker ) + { + // ACHIEVEMENT_TF_MEDIC_KILL_HEALED_SPY - medic kills a spy he has been healing + if ( IsPlayerClass( TF_CLASS_SPY ) && pPlayerAttacker->IsPlayerClass( TF_CLASS_MEDIC ) ) + { + // if we were killed by a medic, see if he healed us most recently + + for ( int i=0;i<pPlayerAttacker->WeaponCount();i++ ) + { + CTFWeaponBase *pWpn = ( CTFWeaponBase *)pPlayerAttacker->GetWeapon( i ); + + if ( pWpn == NULL ) + continue; + + if ( pWpn->GetWeaponID() == TF_WEAPON_MEDIGUN ) + { + CWeaponMedigun *pMedigun = dynamic_cast< CWeaponMedigun * >( pWpn ); + if ( pMedigun ) + { + if ( pMedigun->GetMostRecentHealTarget() == this ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_MEDIC_KILL_HEALED_SPY ); + } + } + } + } + } + + if ( bBurning && pPlayerAttacker->IsPlayerClass( TF_CLASS_PYRO ) ) + { + // ACHIEVEMENT_TF_PYRO_KILL_MULTIWEAPONS - Pyro kills previously ignited target with other weapon + CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>(info.GetWeapon()); + + if ( ( pOriginalBurner == pPlayerAttacker || pLastBurner == pPlayerAttacker ) && pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_SHOTGUN_PYRO ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_PYRO_KILL_MULTIWEAPONS ); + } + + // ACHIEVEMENT_TF_PYRO_KILL_TEAMWORK - Pyro kills an enemy previously ignited by another Pyro + if ( pOriginalBurner != pPlayerAttacker ) + { + pPlayerAttacker->AwardAchievement( ACHIEVEMENT_TF_PYRO_KILL_TEAMWORK ); + } + } + } + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + // Have teammates announce my death + if ( GetTeamNumber() == TF_TEAM_PVE_DEFENDERS ) + { + // have the last player on the defenders speak the last_man_standing line + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS, true ); + if ( playerVector.Count() == 1 ) + { + CTFPlayer *pAlivePlayer = playerVector[0]; + if ( pAlivePlayer ) + { + pAlivePlayer->SpeakConceptIfAllowed( MP_CONCEPT_MVM_LAST_MAN_STANDING ); + } + } + else + { + if ( pPlayerAttacker && pPlayerAttacker->IsMiniBoss() ) + { + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_GIANT_KILLED_TEAMMATE, TF_TEAM_PVE_DEFENDERS ); + } + + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_DEFENDER_DIED, TF_TEAM_PVE_DEFENDERS, CFmtStr( "victimclass:%s", g_aPlayerClassNames_NonLocalized[ GetPlayerClass()->GetClassIndex() ] ).Access() ); + } + } + else + { + if ( IsMiniBoss() ) + { + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_GIANT_KILLED, TF_TEAM_PVE_DEFENDERS ); + } + } + } + + // Reset Streaks to zero + m_Shared.ResetStreaks(); + for ( int i = 0; i < WeaponCount(); i++) + { + CTFWeaponBase *pWpn = ( CTFWeaponBase *)GetWeapon(i); + if ( !pWpn ) + continue; + pWpn->SetKillStreak( 0 ); + } + + for ( int i = 0; i < GetNumWearables(); ++i ) + { + CTFWearable* pWearable = dynamic_cast<CTFWearable*>( GetWearable(i) ); + if ( !pWearable ) + continue; + pWearable->SetKillStreak( 0 ); + } + + // Is the player inside a respawn time override volume? + // don't do this for MvM bots + if ( !TFGameRules()->IsMannVsMachineMode() || !IsBot() ) + { + FOR_EACH_VEC( ITriggerPlayerRespawnOverride::AutoList(), i ) + { + CTriggerPlayerRespawnOverride *pTriggerRespawn = static_cast< CTriggerPlayerRespawnOverride* >( ITriggerPlayerRespawnOverride::AutoList()[i] ); + if ( !pTriggerRespawn->m_bDisabled && pTriggerRespawn->IsTouching( this ) ) + { + SetRespawnOverride( pTriggerRespawn->GetRespawnTime(), pTriggerRespawn->GetRespawnName() ); + break; + } + else + { + SetRespawnOverride( -1.f, NULL_STRING ); + } + } + } + + // Is this an environmental death? + if ( ( info.GetAttacker() == info.GetInflictor() && info.GetAttacker() && info.GetAttacker()->IsBSPModel() ) || + ( info.GetDamageCustom() == TF_DMG_CUSTOM_TRIGGER_HURT ) || + ( info.GetDamageType() & DMG_VEHICLE ) ) + { + CTFPlayer *pRecentDamager = TFGameRules()->GetRecentDamager( this, 1, 5.0 ); + if ( pRecentDamager ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "environmental_death" ); + if ( event ) + { + event->SetInt( "killer", pRecentDamager->entindex() ); + event->SetInt( "victim", entindex() ); + event->SetInt( "priority", 9 ); + + gameeventmanager->FireEvent( event ); + } + } + } + + // make sure to remove custom attributes + RemoveAllCustomAttributes(); +} + +struct SkillRatingAttackRecord_t +{ + CHandle< CTFPlayer > hAttacker; + float flDamagePercent; +}; + +//----------------------------------------------------------------------------- +// 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 ); + + // need to fix up the z because the weapon bone position can be under the player + if ( IsTaunting() ) + { + // put the pack at the middle of the dying player + vecOrigin = WorldSpaceCenter(); + } + + // Draw the position and angles. + Vector vecDebugForward2, vecDebugRight2, vecDebugUp2; + AngleVectors( vecAngles, &vecDebugForward2, &vecDebugRight2, &vecDebugUp2 ); + + /* + NDebugOverlay::Line( vecOrigin, ( vecOrigin + vecDebugForward2 * 25.0f ), 255, 0, 0, false, 30.0f ); + NDebugOverlay::Line( vecOrigin, ( vecOrigin + vecDebugRight2 * 25.0f ), 0, 255, 0, false, 30.0f ); + NDebugOverlay::Line( vecOrigin, ( vecOrigin + vecDebugUp2 * 25.0f ), 0, 0, 255, false, 30.0f ); + */ + + VectorAngles( vecDebugUp2, vecAngles ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// NOTE: If we don't let players drop ammo boxes, we don't need this code.. +//----------------------------------------------------------------------------- +void CTFPlayer::AmmoPackCleanUp( void ) +{ + // If we have more than 3 ammo packs out now, destroy the oldest one. + int iNumPacks = 0; + CTFAmmoPack *pOldestBox = NULL; + + // Cycle through all ammobox in the world and remove them + CBaseEntity *pEnt = gEntList.FindEntityByClassname( NULL, "tf_ammo_pack" ); + while ( pEnt ) + { + CBaseEntity *pOwner = pEnt->GetOwnerEntity(); + if (pOwner == this) + { + CTFAmmoPack *pThisBox = dynamic_cast<CTFAmmoPack *>( pEnt ); + Assert( pThisBox ); + if ( pThisBox ) + { + iNumPacks++; + + // Find the oldest one + if ( pOldestBox == NULL || pOldestBox->GetCreationTime() > pThisBox->GetCreationTime() ) + { + pOldestBox = pThisBox; + } + } + } + + pEnt = gEntList.FindEntityByClassname( pEnt, "tf_ammo_pack" ); + } + + // If they have more than 3 packs active, remove the oldest one + if ( iNumPacks > 3 && pOldestBox ) + { + UTIL_Remove( pOldestBox ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::ShouldDropAmmoPack() +{ + if ( TFGameRules()->IsMannVsMachineMode() && IsBot() ) + return false; + + if ( TFGameRules()->IsInArenaMode() && TFGameRules()->InStalemate() == false ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::DropAmmoPack( const CTakeDamageInfo &info, bool bEmpty, bool bDisguisedWeapon ) +{ + // 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; + + // Figure out which model/skin to use for the drop. We may pull from our real weapon or + // from the weapon we're disguised as. + CTFWeaponBase *pDropWeaponProps = (bDisguisedWeapon && m_Shared.InCond( TF_COND_DISGUISED ) && m_Shared.GetDisguiseWeapon()) + ? m_Shared.GetDisguiseWeapon() + : pWeapon; + + const char *pszWorldModel = pDropWeaponProps->GetWorldModel(); + int nSkin = pDropWeaponProps->GetDropSkinOverride(); + + if ( nSkin < 0 ) + { + nSkin = pDropWeaponProps->GetSkin(); + } + + if ( pszWorldModel == NULL ) + return; + + // Find the position and angle of the weapons so the "ammo box" matches. + Vector vecPackOrigin; + QAngle vecPackAngles; + if( !CalculateAmmoPackPositionAndAngles( pWeapon, vecPackOrigin, vecPackAngles ) ) + return; + + CEconItemView *pItem = pDropWeaponProps->GetAttributeContainer()->GetItem(); + bool bIsSuicide = info.GetAttacker() ? info.GetAttacker()->GetTeamNumber() == GetTeamNumber() : false; + + CTFDroppedWeapon *pDroppedWeapon = CTFDroppedWeapon::Create( this, vecPackOrigin, vecPackAngles, pszWorldModel, pItem ); + if ( pDroppedWeapon ) + { + pDroppedWeapon->InitDroppedWeapon( this, pDropWeaponProps, false, bIsSuicide ); + } + + // Create the ammo pack. + CTFAmmoPack *pAmmoPack = CTFAmmoPack::Create( vecPackOrigin, vecPackAngles, this, "models/items/ammopack_medium.mdl" ); + Assert( pAmmoPack ); + if ( pAmmoPack ) + { + pAmmoPack->InitWeaponDrop( this, pWeapon, nSkin, bEmpty, bIsSuicide ); + + // Clean up old ammo packs if they exist in the world + AmmoPackCleanUp(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::DropHealthPack( const CTakeDamageInfo &info, bool bEmpty ) +{ + Vector vecSrc = this->WorldSpaceCenter(); + CHealthKitSmall *pMedKit = assert_cast<CHealthKitSmall*>( CBaseEntity::Create( "item_healthkit_small", vecSrc, vec3_angle, this ) ); + if ( pMedKit ) + { + Vector vecImpulse = RandomVector( -1,1 ); + vecImpulse.z = 1; + VectorNormalize( vecImpulse ); + + Vector vecVelocity = vecImpulse * 250.0; + pMedKit->DropSingleInstance( vecVelocity, this, 0 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::DropCurrencyPack( CurrencyRewards_t nSize /* = TF_CURRENCY_PACK_SMALL */, int nAmount /*= 0*/, bool bForceDistribute /*= false*/, CBasePlayer* pMoneyMaker /*= NULL*/ ) +{ + // SMALL, MEDIUM, LARGE packs generate a default value on spawn + // Only pass in an amount when dropping TF_CURRENCY_PACK_CUSTOM + + Vector vecSrc = this->WorldSpaceCenter(); + CCurrencyPack *pCurrencyPack = NULL; + + switch ( nSize ) + { + case TF_CURRENCY_PACK_SMALL: + pCurrencyPack = assert_cast<CCurrencyPackSmall*>( CBaseEntity::Create( "item_currencypack_small", vecSrc, vec3_angle, this ) ); + break; + + case TF_CURRENCY_PACK_MEDIUM: + pCurrencyPack = assert_cast<CCurrencyPackMedium*>( CBaseEntity::Create( "item_currencypack_medium", vecSrc, vec3_angle, this ) ); + break; + + case TF_CURRENCY_PACK_LARGE: + pCurrencyPack = assert_cast<CCurrencyPack*>( CBaseEntity::Create( "item_currencypack_large", vecSrc, vec3_angle, this ) ); + break; + + case TF_CURRENCY_PACK_CUSTOM: + // Pop file may have said to not drop anything + Assert( nAmount > 0 ); + if ( nAmount == 0 ) + return; + + // Create no spawn first so we can set the multiplier before it spawns & picks it model + pCurrencyPack = assert_cast<CCurrencyPack*>( CBaseEntity::CreateNoSpawn( "item_currencypack_custom", vecSrc, vec3_angle, this ) ); + pCurrencyPack->SetAmount( nAmount ); + break; + }; + + if ( pCurrencyPack ) + { + Vector vecImpulse = RandomVector( -1,1 ); + vecImpulse.z = 1; + VectorNormalize( vecImpulse ); + Vector vecVelocity = vecImpulse * 250.0; + + if ( pMoneyMaker || bForceDistribute ) + { + pCurrencyPack->DistributedBy( pMoneyMaker ); + } + + DispatchSpawn( pCurrencyPack ); + pCurrencyPack->DropSingleInstance( vecVelocity, this, 0, 0 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::PlayerDeathThink( void ) +{ + // We're doing this here to avoid getting stuck + // in a recursive loop if we do it in Event_Killed + if ( m_bPendingMerasmusPlayerBombExplode ) + { + m_bPendingMerasmusPlayerBombExplode = false; + MerasmusPlayerBombExplode(); + } + + // don't need to think again... +} + +//----------------------------------------------------------------------------- +// Purpose: Remove the tf items from the player then call into the base class +// removal of items. +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveAllItems( bool removeSuit ) +{ + if ( TFGameRules() && TFGameRules()->IsPasstimeMode() && m_Shared.HasPasstimeBall() ) + { + g_pPasstimeLogic->EjectBall( this, this ); + } + + // If the player has a capture flag, drop it. + if ( HasItem() ) + { + int nFlagTeamNumber = GetItem()->GetTeamNumber(); + 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 ); + event->SetInt( "team", nFlagTeamNumber ); + gameeventmanager->FireEvent( event ); + } + } + + m_Shared.Heal_Radius( false ); + + 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(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ClientHearVox( const char *pSentence ) +{ + //TFTODO: implement this. +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::UpdateModel( void ) +{ + SetModel( GetPlayerClass()->GetModelName() ); + + // Immediately reset our collision bounds - our collision bounds will be set to the model's bounds. + SetCollisionBounds( GetPlayerMins(), GetPlayerMaxs() ); + + m_PlayerAnimState->OnNewModel(); +} + +//----------------------------------------------------------------------------- +// 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 +} + +void CTFPlayer::SetIsCoaching( bool bIsCoaching ) +{ + m_bIsCoaching = bIsCoaching; + + if ( !bIsCoaching ) + { + // reset our last action time so we don't get kicked for being idle while we were coaching + m_flLastAction = gpGlobals->curtime; + } +} + +//========================================================================= +// Called when the player disconnects from the server. +void CTFPlayer::TeamFortress_ClientDisconnected( void ) +{ + RemoveAllOwnedEntitiesFromWorld( true ); + RemoveNemesisRelationships(); + + StopTaunt(); + + RemoveAllWeapons(); + + RemoveAllItems( true ); + + TFGameRules()->RemovePlayerFromQueue( this ); + TFGameRules()->PlayerHistory_AddPlayer( this ); + + DuelMiniGame_NotifyPlayerDisconnect( this ); + + // cleanup coaching + if ( GetCoach() ) + { + GetCoach()->SetIsCoaching( false ); + GetCoach()->SetStudent( NULL ); + } + else if ( GetStudent() ) + { + SetIsCoaching( false ); + GetStudent()->SetCoach( NULL ); + } + + // Drop your powerup when you disconnect + if ( m_Shared.IsCarryingRune() ) + { + CTFRune::CreateRune( GetAbsOrigin(), m_Shared.GetCarryingRuneType(), TEAM_ANY, true, false ); + } +} + +//========================================================================= +// Removes everything this player has (buildings, grenades, etc.) from the world +void CTFPlayer::RemoveAllOwnedEntitiesFromWorld( bool bExplodeBuildings /* = false */ ) +{ + RemoveOwnedProjectiles(); + + if ( TFGameRules()->IsMannVsMachineMode() && ( GetTeamNumber() == TF_TEAM_PVE_INVADERS ) ) + { + // MvM engineer bots leave their sentries behind when they die + return; + } + + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() && ( GetTeamNumber() == TF_TEAM_RED ) ) + { + // for now, leave Engineer's sentrygun alive after he dies + return; + } +#endif // TF_RAID_MODE + + if ( IsBotOfType( TF_BOT_TYPE ) && ToTFBot( this )->HasAttribute( CTFBot::RETAIN_BUILDINGS ) ) + { + // keep this bot's buildings + return; + } + + // Destroy any buildables - this should replace TeamFortress_RemoveBuildings + RemoveAllObjects( bExplodeBuildings ); +} + +//========================================================================= +// 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::RemoveOwnedProjectiles( void ) +{ + FOR_EACH_VEC( IBaseProjectileAutoList::AutoList(), i ) + { + CBaseProjectile *pProjectile = static_cast< CBaseProjectile* >( IBaseProjectileAutoList::AutoList()[i] ); + + // if the player owns this entity, remove it + bool bOwner = ( pProjectile->GetOwnerEntity() == this ); + + if ( !bOwner ) + { + if ( pProjectile->GetBaseProjectileType() == TF_BASE_PROJECTILE_GRENADE ) + { + + CTFWeaponBaseGrenadeProj *pGrenade = assert_cast<CTFWeaponBaseGrenadeProj*>( pProjectile ); + if ( pGrenade ) + { + bOwner = ( pGrenade->GetThrower() == this ); + } + } + else if ( pProjectile->GetProjectileType() == TF_PROJECTILE_SENTRY_ROCKET ) + { + CTFProjectile_SentryRocket *pRocket = assert_cast<CTFProjectile_SentryRocket*>( pProjectile ); + if ( pRocket ) + { + bOwner = ( pRocket->GetScorer() == this ); + } + } + } + + if ( bOwner ) + { + pProjectile->SetTouch( NULL ); + pProjectile->AddEffects( EF_NODRAW ); + UTIL_Remove( pProjectile ); + } + } + + FOR_EACH_VEC( ITFFlameEntityAutoList::AutoList(), i ) + { + CTFFlameEntity *pFlameEnt = static_cast< CTFFlameEntity* >( ITFFlameEntityAutoList::AutoList()[i] ); + + if ( pFlameEnt->IsEntityAttacker( this ) ) + { + pFlameEnt->SetTouch( NULL ); + pFlameEnt->AddEffects( EF_NODRAW ); + UTIL_Remove( pFlameEnt ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::NoteWeaponFired() +{ + Assert( m_pCurrentCommand ); + if ( m_pCurrentCommand ) + { + m_iLastWeaponFireUsercmd = m_pCurrentCommand->command_number; + } + + // Remember the tickcount when the weapon was fired and lock viewangles here! + if ( m_iLockViewanglesTickNumber != gpGlobals->tickcount ) + { + m_iLockViewanglesTickNumber = gpGlobals->tickcount; + m_qangLockViewangles = pl.v_angle; + } +} + +//============================================================================= +// +// 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 ( g_pServerBenchmark->IsLocalBenchmarkPlayer( this ) ) + { + m_bSeenRoundInfo = true; + + ChangeTeam( TEAM_SPECTATOR ); + } + else 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; + } +//============================================================================= +// HPE_BEGIN: +// [msmith] When in training, we want the option to show an intro movie. +//============================================================================= + else if ( TFGameRules()->IsInTraining() && IsFakeClient() == false ) + { + ShowViewPortPanel( PANEL_INTRO, true ); + m_bSeenRoundInfo = true; + } +//============================================================================= +// HPE_END +//============================================================================= +#ifdef STAGING_ONLY + else if ( tf_skip_intro_and_spectate.GetBool() ) + { + m_bSeenRoundInfo = true; + ChangeTeam( TEAM_SPECTATOR ); + SetObserverMode( OBS_MODE_CHASE ); + } +#endif + else + { + if ( !IsX360() ) + { + char pszWelcome[128]; + Q_snprintf( pszWelcome, sizeof(pszWelcome), "#TF_Welcome" ); + if ( UTIL_GetActiveHolidayString() ) + { + Q_snprintf( pszWelcome, sizeof(pszWelcome), "#TF_Welcome_%s", UTIL_GetActiveHolidayString() ); + } + + KeyValues *data = new KeyValues( "data" ); + data->SetString( "title", pszWelcome ); // info panel title + data->SetString( "type", "1" ); // show userdata from stringtable entry + data->SetString( "msg", "motd" ); // use this stringtable entry + data->SetString( "msg_fallback", "motd_text" ); // use this stringtable entry if the base is HTML, and client has disabled HTML motds + data->SetBool( "unload", sv_motd_unload_on_dismissal.GetBool() ); + + ShowViewPortPanel( PANEL_INFO, true, data ); + + data->deleteThis(); + } + else + { + ShowViewPortPanel( PANEL_MAPINFO, true ); + } + + m_bSeenRoundInfo = false; + } + +#ifdef STAGING_ONLY + if ( TFGameRules() && TFGameRules()->IsBountyMode() ) + { + // See if we should give starting money + int nCurrency = tf_bountymode_currency_starting.GetInt(); + if ( nCurrency > 0 ) + { + SetCurrency( nCurrency ); + } + } +#endif // STAGING_ONLY +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::StateThinkWELCOME( void ) +{ + if ( !IsFakeClient() ) + { + if ( IsInCommentaryMode() ) + { + ChangeTeam( TF_TEAM_BLUE ); + SetDesiredPlayerClassIndex( TF_CLASS_SCOUT ); + ForceRespawn(); + } + else if ( TFGameRules()->IsInTraining() ) + { + int iTeam = TFGameRules()->GetAssignedHumanTeam(); + int iClass = TFGameRules()->GetTrainingModeLogic() ? TFGameRules()->GetTrainingModeLogic()->GetDesiredClass() : TF_CLASS_SOLDIER; + ChangeTeam( iTeam != TEAM_ANY ? iTeam : TF_TEAM_BLUE ); + SetDesiredPlayerClassIndex( iClass ); + 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_flLastHealthRegenAt = gpGlobals->curtime; + SetContextThink( &CTFPlayer::RegenThink, gpGlobals->curtime + TF_REGEN_TIME, "RegenThink" ); + if ( TFGameRules() && TFGameRules()->IsPowerupMode() ) + { + SetContextThink( &CTFPlayer::RuneRegenThink, gpGlobals->curtime + TF_REGEN_TIME_RUNE, "RuneRegenThink" ); + } + +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::SetObserverMode(int mode) +{ + if ( !TFGameRules() ) + return false; + + if ( mode < OBS_MODE_NONE || mode >= NUM_OBSERVER_MODES ) + return false; + + if ( TFGameRules()->ShowMatchSummary() ) + return false; + + // Skip over OBS_MODE_POI if we're not in Passtime mode + if ( mode == OBS_MODE_POI ) + { + if ( !TFGameRules()->IsPasstimeMode() ) + { + mode = OBS_MODE_ROAMING; + } + } + + // 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; + + if ( !m_bArenaIsAFK ) + { + m_flLastAction = gpGlobals->curtime; + } + + // this is the old behavior, still supported for community servers + bool bAllowSpecModeChange = TFGameRules()->IsInTournamentMode() ? TFGameRules()->IsMannVsMachineMode() : true; + + // new behavior for Valve casual, competitive, and mvm matches + const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( TFGameRules()->GetCurrentMatchGroup() ); + if ( pMatchDesc ) + { + bAllowSpecModeChange = pMatchDesc->m_params.m_bAllowSpecModeChange; + } + + if ( !bAllowSpecModeChange ) + { + if ( ( mode != OBS_MODE_DEATHCAM ) && ( mode != OBS_MODE_FREEZECAM ) && ( GetTeamNumber() > TEAM_SPECTATOR ) ) + { + if ( IsValidObserverTarget( GetObserverTarget() ) ) + { + m_iObserverMode.Set( OBS_MODE_IN_EYE ); + } + else + { + m_iObserverMode.Set( OBS_MODE_DEATHCAM ); + } + } + } + + switch ( m_iObserverMode ) + { + 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_POI : // PASSTIME + SetObserverTarget( TFGameRules()->GetObjectiveObserverTarget() ); + 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(); + } + + // If we haven't yet set a valid observer mode, such as when + // the player aborts the freezecam and sets a mode "by hand" + // force the initial mode to last mode + if ( m_iObserverMode <= OBS_MODE_FREEZECAM ) + { + if ( TFGameRules() && TFGameRules()->IsPasstimeMode() ) + { + m_iObserverMode = OBS_MODE_POI; + } + else + { + m_iObserverMode = m_iObserverLastMode; + } + } + + // If we're in fixed mode, but we found an observer target, move to non fixed. + if ( m_hObserverTarget.Get() != NULL && m_iObserverMode == OBS_MODE_FIXED ) + { + m_iObserverMode.Set( OBS_MODE_IN_EYE ); + } + + StartObserverMode( m_iObserverMode ); + + PhysObjectSleep(); + + 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 ); + +#ifdef STAGING_ONLY + if ( tf_skip_intro_and_spectate.GetInt() > 5 ) + { + static float s_flLastTime = gpGlobals->curtime; + float curtime = gpGlobals->curtime; + + if ( ( curtime - s_flLastTime ) > tf_skip_intro_and_spectate.GetInt() ) + { + s_flLastTime = curtime; + + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *pl = UTIL_PlayerByIndex( i ); + + if ( pl && ( pl->GetTeamNumber() == TEAM_SPECTATOR ) ) + { + CBaseEntity * target = pl->FindNextObserverTarget( false ); + if ( target ) + { + // Could also switch spec_mode: GetObserverMode(). + pl->SetObserverTarget( target ); + } + } + } + } + } +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::StateEnterDYING( void ) +{ + SetMoveType( MOVETYPE_NONE ); + AddSolidFlags( FSOLID_NOT_SOLID ); + + m_bPlayedFreezeCamSound = false; + m_bAbortFreezeCam = false; + + if ( TFGameRules() && TFGameRules()->IsInArenaMode() ) + { + float flLastActionTime = gpGlobals->curtime - m_flLastAction; + float flAliveThisRoundTime = gpGlobals->curtime - TFGameRules()->GetRoundStart(); + + if ( flAliveThisRoundTime - flLastActionTime < 0 ) + { + m_bArenaIsAFK = true; + } + } +} + +//----------------------------------------------------------------------------- +// 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(); + + if ( TFGameRules() && TFGameRules()->IsPasstimeMode() ) + { + SetObserverMode( OBS_MODE_POI ); + } + else + { + 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(); + + IncrementInterpolationFrame(); + + 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 ) +{ + return GiveAmmo( iCount, iAmmoIndex, bSuppressSound, kAmmoSource_Pickup ); +} + +//----------------------------------------------------------------------------- +// 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, EAmmoSource eAmmoSource ) +{ + if ( iCount <= 0 ) + { + return 0; + } + + // Metal always ignores the eAmmoSource settings, which are really used only for determining + // whether ammo should be converted into health or ignored or, in rare cases, treated as actual + // ammo. + if ( iAmmoIndex != TF_AMMO_METAL ) + { + //int iAmmoBecomesHealth = 0; + //CALL_ATTRIB_HOOK_INT( iAmmoBecomesHealth, ammo_becomes_health ); + //if ( iAmmoBecomesHealth == 1 ) + //{ + // // Ammo from ground pickups is converted to health. + // if ( eAmmoSource == kAmmoSource_Pickup ) + // { + // int iTakenHealth = TakeHealth( iCount, DMG_GENERIC ); + // if ( iTakenHealth > 0 ) + // { + // if ( !bSuppressSound ) + // { + // EmitSound( "BaseCombatCharacter.AmmoPickup" ); + // } + // m_Shared.HealthKitPickupEffects( iCount ); + // } + // return iTakenHealth; + // } + + // // Ammo from the cart or engineer dispensers is flatly ignored. + // if ( eAmmoSource == kAmmoSource_DispenserOrCart ) + // return 0; + + // Assert( eAmmoSource == kAmmoSource_Resupply ); + //} + } + else if ( iAmmoIndex == TF_AMMO_METAL ) + { + if ( eAmmoSource != kAmmoSource_Resupply ) + { + float flMultMetal = 1.0f; + CALL_ATTRIB_HOOK_FLOAT( flMultMetal, mult_metal_pickup ); + iCount = (int)(flMultMetal * iCount ); + } + } + + + 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 iAdd = MIN( iCount, GetMaxAmmo(iAmmoIndex) - GetAmmoCount(iAmmoIndex) ); + if ( iAdd < 1 ) + { + return 0; + } + + // Ammo pickup sound + if ( !bSuppressSound ) + { + EmitSound( "BaseCombatCharacter.AmmoPickup" ); + } + + CBaseCombatCharacter::GiveAmmo( iAdd, iAmmoIndex, bSuppressSound ); + return iAdd; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveAmmo( int iCount, int iAmmoIndex ) +{ +#ifdef STAGING_ONLY + if ( tf_infinite_ammo.GetBool() ) + { + return; + } +#endif // STAGING_ONLY + +#if defined( _DEBUG ) || defined( STAGING_ONLY ) + if ( mp_developer.GetInt() > 1 && !IsBot() ) + return; +#endif // _DEBUG || STAGING_ONLY + + if ( m_Shared.InCond( TF_COND_HALLOWEEN_GIANT ) ) + { + return; + } + + // Infinite primary, secondary and metal in these game modes + if ( TFGameRules() && iAmmoIndex < TF_AMMO_GRENADES1 ) + { + if ( TFGameRules()->IsMannVsMachineMode() && GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + return; + +#ifdef STAGING_ONLY + if ( TFGameRules()->IsBountyMode() && IsMiniBoss() ) + return; +#endif // STAGING_ONLY + } + + CBaseCombatCharacter::RemoveAmmo( iCount, iAmmoIndex ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveAmmo( int iCount, const char *szName ) +{ + if ( TFGameRules() ) + { + if ( TFGameRules()->IsMannVsMachineMode() && GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + return; + + if ( TFGameRules()->GameModeUsesMiniBosses() && IsMiniBoss() ) + return; + } + + CBaseCombatCharacter::RemoveAmmo( iCount, szName ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the amount of ammunition of a particular type owned +// owned by the character +// Input : Ammo Index +// Output : The amount of ammo +//----------------------------------------------------------------------------- +int CTFPlayer::GetAmmoCount( int iAmmoIndex ) const +{ + if ( iAmmoIndex == -1 ) + return 0; + + if ( IsFakeClient() && TFGameRules()->IsInItemTestingMode() ) + return 999; + + return BaseClass::GetAmmoCount( iAmmoIndex ); +} + +//----------------------------------------------------------------------------- +// Purpose: Has to be const for override, but needs to access non-const member methods. +//----------------------------------------------------------------------------- +int CTFPlayer::GetMaxHealth() const +{ + int iMax = GetMaxHealthForBuffing(); + + // Also add the nonbuffed health bonuses + CALL_ATTRIB_HOOK_INT( iMax, add_maxhealth_nonbuffed ); + + return MAX( iMax, 1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTFPlayer::GetMaxHealthForBuffing() const +{ + int iMax = m_PlayerClass.GetMaxHealth(); + CALL_ATTRIB_HOOK_INT( iMax, add_maxhealth ); + + CTFWeaponBase *pWeapon = GetActiveTFWeapon(); + if ( pWeapon ) + { + iMax += pWeapon->GetMaxHealthMod(); + } + if ( const_cast<CTFPlayer*>(this)->GetPlayerClass()->GetClassIndex() == TF_CLASS_DEMOMAN ) + { + CTFSword *pSword = dynamic_cast<CTFSword*>(const_cast<CTFPlayer*>(this)->Weapon_OwnsThisID( TF_WEAPON_SWORD )); + if ( pSword ) + { + iMax += pSword->GetSwordHealthMod(); + } + } + + // Some Powerup Runes increase your Max Health + iMax += GetRuneHealthBonus(); + + if ( m_Shared.InCond( TF_COND_HALLOWEEN_GIANT ) ) + { + return iMax * tf_halloween_giant_health_scale.GetFloat(); + } + + return iMax; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTFPlayer::GetRuneHealthBonus() const +{ + int nRuneType = m_Shared.GetCarryingRuneType(); + + if ( nRuneType == RUNE_NONE ) + { + return 0; + } + + if ( nRuneType == RUNE_KNOCKOUT ) + { + if ( IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + // Swords have various extra melee benefits, so we reduce Max Health bonus + if ( Weapon_GetSlot( TF_WPN_TYPE_MELEE ) ) + { + int iDecapitateType = 0; + CALL_ATTRIB_HOOK_INT( iDecapitateType, decapitate_type ); + + if ( iDecapitateType ) + { + return 20; + } + } + // Shields have passive resistance so we reduce Max Health bonus + if ( m_Shared.IsShieldEquipped() ) + { + return 30; + } + return 150; + } + else if ( IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) || IsPlayerClass( TF_CLASS_PYRO ) ) + { + return 125; + } + else if ( IsPlayerClass( TF_CLASS_SOLDIER ) || IsPlayerClass( TF_CLASS_MEDIC ) ) + { + return 150; + } + else + { + return 175; + } + } + else if ( nRuneType == RUNE_REFLECT ) + { + return ( 400 - m_PlayerClass.GetMaxHealth() ); + } + else if ( nRuneType == RUNE_KING ) + { + return 100; + } + else if ( nRuneType == RUNE_VAMPIRE ) + { + return 80; + } + + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ForceRegenerateAndRespawn( void ) +{ + m_bRegenerating = true; + ForceRespawn(); + m_bRegenerating = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Reset player's information and force him to spawn +//----------------------------------------------------------------------------- +void CTFPlayer::ForceRespawn( void ) +{ + VPROF_BUDGET( "CTFPlayer::ForceRespawn", VPROF_BUDGETGROUP_PLAYER ); + + CTF_GameStats.Event_PlayerForceRespawn( this ); + + m_flSpawnTime = gpGlobals->curtime; + + bool bRandom = false; + + // force a random class if the server requires it + if ( TFGameRules() && TFGameRules()->IsInArenaMode() ) + { + if ( tf_arena_force_class.GetBool() == true ) + { + bRandom = true; + if ( GetTeamNumber() > LAST_SHARED_TEAM ) + { + if ( !IsAlive() || ( TFGameRules() && TFGameRules()->State_Get() != GR_STATE_STALEMATE ) ) + { + HandleCommand_JoinClass( "random", false ); + } + } + } + + if ( ( tf_arena_use_queue.GetBool() == false && TFGameRules()->IsInWaitingForPlayers() ) || TFGameRules()->State_Get() == GR_STATE_PREGAME ) + { + return; + } + } + + int iDesiredClass = GetDesiredPlayerClassIndex(); + + if ( iDesiredClass == TF_CLASS_UNDEFINED ) + { + return; + } + + if ( iDesiredClass == TF_CLASS_RANDOM ) + { + bRandom = true; + + // 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) + m_bSwitchedClass = true; + + RemoveAllOwnedEntitiesFromWorld(); + + int iOldClass = GetPlayerClass()->GetClassIndex(); + + GetPlayerClass()->Init( iDesiredClass ); + + // Don't report class changes if we're random, because it's not a player choice + if ( !bRandom ) + { + m_iClassChanges++; + CTF_GameStats.Event_PlayerChangedClass( this, iOldClass, iDesiredClass ); + } + } + else + { + m_bSwitchedClass = false; + } + + m_Shared.RemoveAllCond(); + m_Shared.ResetRageMeter(); + + if ( m_bSwitchedClass ) + { + m_iLastWeaponSlot = 1; + // Tell all the items we have that we've changed class. Some items need to change model. + // Also reset KillStreaks + for ( int i = 0; i < MAX_WEAPONS; i++ ) + { + CTFWeaponBase *pWeapon = (CTFWeaponBase *)GetWeapon(i); + if ( pWeapon ) + { + pWeapon->OnOwnerClassChange(); + } + } + } + else + { + if ( IsAlive() ) + { + if ( GetActiveTFWeapon() ) + { + m_iActiveWeaponTypePriorToDeath = GetActiveTFWeapon()->GetWeaponID(); + } + SaveLastWeaponSlot(); + } + } + + // Any Respawns will reset killstreaks + m_Shared.ResetStreaks(); + for ( int i = 0; i < WeaponCount(); i++ ) + { + CTFWeaponBase *pWpn = (CTFWeaponBase *)GetWeapon( i ); + if ( !pWpn ) + continue; + pWpn->SetKillStreak( 0 ); + } + + for ( int i = 0; i < GetNumWearables(); ++i ) + { + CTFWearable* pWearable = dynamic_cast<CTFWearable*>( GetWearable( i ) ); + if ( !pWearable ) + continue; + pWearable->SetKillStreak( 0 ); + } + + RemoveAllItems( true ); + + // Reset ground state for airwalk animations + SetGroundEntity( NULL ); + + // TODO: move this into conditions + RemoveTeleportEffect(); + + // remove invisibility very quickly + m_Shared.FadeInvis( 0.1f ); + + // Stop any firing that was taking place before respawn. + m_nButtons = 0; + + StateTransition( TF_STATE_ACTIVE ); + Spawn(); + m_bSwitchedClass = false; +} + +//----------------------------------------------------------------------------- +// 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 ); + GiveAmmo( 1000, TF_AMMO_GRENADES1 ); + GiveAmmo( 1000, TF_AMMO_GRENADES2 ); + GiveAmmo( 1000, TF_AMMO_GRENADES3 ); + TakeHealth( 999, DMG_GENERIC ); + + // Refills weapon clips, too + for ( int i = 0; i < MAX_WEAPONS; i++ ) + { + CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase* >( GetWeapon( i ) ); + if ( !pWeapon ) + continue; + + pWeapon->GiveDefaultAmmo(); + + if ( pWeapon->IsEnergyWeapon() ) + { + pWeapon->WeaponRegenerate(); + } + } + + m_Shared.m_flRageMeter = 100.f; + m_Shared.SetDemomanChargeMeter( 100.f ); + + 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; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveBuildResources( int iAmount ) +{ + RemoveAmmo( iAmount, TF_AMMO_METAL ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::AddBuildResources( int iAmount ) +{ + GiveAmmo( iAmount, TF_AMMO_METAL, false, kAmmoSource_Pickup ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseObject *CTFPlayer::GetObject( int index ) const +{ + return (CBaseObject *)( m_aObjects[index].Get() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseObject *CTFPlayer::GetObjectOfType( int iObjectType, int iObjectMode ) const +{ + int iNumObjects = GetObjectCount(); + for ( int i=0; i<iNumObjects; i++ ) + { + CBaseObject *pObj = GetObject(i); + + if ( !pObj ) + continue; + + if ( pObj->GetType() != iObjectType ) + continue; + + if ( pObj->GetObjectMode() != iObjectMode ) + continue; + + if ( pObj->IsDisposableBuilding() ) + continue; + + return pObj; + } + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTFPlayer::GetObjectCount( void ) const +{ + return m_aObjects.Count(); +} + +//----------------------------------------------------------------------------- +// Purpose: Remove all the player's objects +// If bExplodeBuildings is not set, remove all of them immediately. +// Otherwise, make them all explode. +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveAllObjects( bool bExplodeBuildings /* = false */ ) +{ + // Remove all the player's objects + for (int i = GetObjectCount()-1; i >= 0; i--) + { + CBaseObject *obj = GetObject(i); + Assert( obj ); + + if ( obj ) + { + // this is separate from the object_destroyed event, which does + // not get sent when we remove the objects from the world + IGameEvent *event = gameeventmanager->CreateEvent( "object_removed" ); + if ( event ) + { + event->SetInt( "userid", GetUserID() ); // user ID of the object owner + event->SetInt( "objecttype", obj->GetType() ); // type of object removed + event->SetInt( "index", obj->entindex() ); // index of the object removed + gameeventmanager->FireEvent( event ); + } + + if ( bExplodeBuildings ) + { + obj->DetonateObject(); + } + else + { + // This fixes a bug in Raid mode where we could spawn where our sentry was but + // we didn't get the weapons because they couldn't trace to us in FVisible + obj->SetSolid( SOLID_NONE ); + 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 = m_Shared.CalculateObjectCost( this, 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 ); + + if ( TFGameRules() && TFGameRules()->IsInTraining() && TFGameRules()->GetTrainingModeLogic() && IsFakeClient() == false ) + { + TFGameRules()->GetTrainingModeLogic()->OnPlayerBuiltBuilding( this, pObject ); + } + + /* + // Tell our builder weapon + CTFWeaponBuilder *pBuilder = GetWeaponBuilder(); + if ( pBuilder ) + { + pBuilder->FinishedObject(); + } + */ +} + +//----------------------------------------------------------------------------- +// Purpose: Add the specified object to this player's object list. +//----------------------------------------------------------------------------- +void CTFPlayer::AddObject( CBaseObject *pObject ) +{ + TRACE_OBJECT( UTIL_VarArgs( "%0.2f CBaseTFPlayer::AddObject adding object %p:%s to player %s\n", gpGlobals->curtime, pObject, pObject->GetClassname(), GetPlayerName() ) ); + + // Make a handle out of it + CHandle<CBaseObject> hObject; + hObject = pObject; + + bool alreadyInList = PlayerOwnsObject( pObject ); + // Assert( !alreadyInList ); + if ( !alreadyInList ) + { + m_aObjects.AddToTail( hObject ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Object built by this player has been destroyed +//----------------------------------------------------------------------------- +void CTFPlayer::OwnedObjectDestroyed( CBaseObject *pObject ) +{ + TRACE_OBJECT( UTIL_VarArgs( "%0.2f CBaseTFPlayer::OwnedObjectDestroyed player %s object %p:%s\n", gpGlobals->curtime, + GetPlayerName(), + pObject, + pObject->GetClassname() ) ); + + RemoveObject( pObject ); + + // Tell our builder weapon so it recalculates the state of the build icons + /* + CTFWeaponBuilder *pBuilder = GetWeaponBuilder(); + if ( pBuilder ) + { + pBuilder->RecalcState(); + } + */ +} + +//----------------------------------------------------------------------------- +// Removes an object from the player +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveObject( CBaseObject *pObject ) +{ + TRACE_OBJECT( UTIL_VarArgs( "%0.2f CBaseTFPlayer::RemoveObject %p:%s from player %s\n", gpGlobals->curtime, + pObject, + pObject->GetClassname(), + GetPlayerName() ) ); + + Assert( pObject ); + + int i; + for ( i = m_aObjects.Count(); --i >= 0; ) + { + // Also, while we're at it, remove all other bogus ones too... + if ( (!m_aObjects[i].Get()) || (m_aObjects[i] == pObject)) + { + m_aObjects.FastRemove(i); + } + } +} + +//----------------------------------------------------------------------------- +// See if the player owns this object +//----------------------------------------------------------------------------- +bool CTFPlayer::PlayerOwnsObject( CBaseObject *pObject ) +{ + return ( m_aObjects.Find( pObject ) != -1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::PlayFlinch( const CTakeDamageInfo &info ) +{ + // Don't play flinches if we just died. + if ( !IsAlive() ) + return; + + // No pain flinches while disguised, our man has supreme discipline + if ( m_Shared.InCond( TF_COND_DISGUISED ) ) + return; + + PlayerAnimEvent_t flinchEvent; + + switch ( LastHitGroup() ) + { + // pick a region-specific flinch + case HITGROUP_HEAD: + flinchEvent = PLAYERANIMEVENT_FLINCH_HEAD; + break; + case HITGROUP_LEFTARM: + flinchEvent = PLAYERANIMEVENT_FLINCH_LEFTARM; + break; + case HITGROUP_RIGHTARM: + flinchEvent = PLAYERANIMEVENT_FLINCH_RIGHTARM; + break; + case HITGROUP_LEFTLEG: + flinchEvent = PLAYERANIMEVENT_FLINCH_LEFTLEG; + break; + case HITGROUP_RIGHTLEG: + flinchEvent = PLAYERANIMEVENT_FLINCH_RIGHTLEG; + break; + case HITGROUP_STOMACH: + case HITGROUP_CHEST: + case HITGROUP_GEAR: + case HITGROUP_GENERIC: + default: + // just get a generic flinch. + flinchEvent = PLAYERANIMEVENT_FLINCH_CHEST; + break; + } + + DoAnimationEvent( flinchEvent ); +} + +//----------------------------------------------------------------------------- +// Purpose: Plays the crit sound that players that get crit hear +//----------------------------------------------------------------------------- +float CTFPlayer::PlayCritReceivedSound( void ) +{ + float flCritPainLength = 0; + // Play a custom pain sound to the guy taking the damage + CSingleUserRecipientFilter receiverfilter( this ); + EmitSound_t params; + params.m_flSoundTime = 0; + params.m_pSoundName = "TFPlayer.CritPain"; + params.m_pflSoundDuration = &flCritPainLength; + EmitSound( receiverfilter, entindex(), params ); + + return flCritPainLength; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::PainSound( const CTakeDamageInfo &info ) +{ + // Don't make sounds if we just died. DeathSound will handle that. + if ( !IsAlive() ) + return; + + // no pain sounds while disguised, our man has supreme discipline + if ( m_Shared.InCond( TF_COND_DISGUISED ) ) + return; + + if ( m_flNextPainSoundTime > gpGlobals->curtime ) + return; + + // Don't play falling pain sounds, they have their own system + if ( info.GetDamageType() & DMG_FALL ) + return; + + // No sound for DMG_GENERIC + if ( info.GetDamageType() == 0 || info.GetDamageType() == DMG_PREVENT_PHYSICS_FORCE ) + 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_bGoingFeignDeath ) + { + bool bDisguised = m_Shared.InCond( TF_COND_DISGUISED ) && (m_Shared.GetDisguiseTeam() == GetTeamNumber()); + if ( bDisguised ) + { + // Use our disguise class, if we have one and will drop a disguise class corpse. + pData = g_pTFPlayerClassDataMgr->Get( m_Shared.GetDisguiseClass() ); + if ( !pData ) + return; + } + } + + CTFPlayer *pAttacker = (CTFPlayer*)ToTFPlayer( info.GetAttacker() ); + if ( pAttacker ) + { + CTFWeaponBase *pWpn = pAttacker->GetActiveTFWeapon(); + if ( pWpn && pWpn->IsSilentKiller() ) + return; + } + + int nDeathSoundOffset = DEATH_SOUND_FIRST; + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + nDeathSoundOffset = IsMiniBoss() ? DEATH_SOUND_GIANT_MVM_FIRST : DEATH_SOUND_MVM_FIRST; + } + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && + GetTeamNumber() != TF_TEAM_PVE_INVADERS && !m_bGoingFeignDeath ) + { + EmitSound( "MVM.PlayerDied" ); + 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->GetDeathSound( DEATH_SOUND_EXPLOSION + nDeathSoundOffset ) ); + } + else if ( m_LastDamageType & DMG_CRITICAL ) + { + EmitSound( pData->GetDeathSound( DEATH_SOUND_CRIT + nDeathSoundOffset ) ); + + PlayCritReceivedSound(); + } + else if ( m_LastDamageType & DMG_CLUB ) + { + EmitSound( pData->GetDeathSound( DEATH_SOUND_MELEE + nDeathSoundOffset ) ); + } + else + { + EmitSound( pData->GetDeathSound( DEATH_SOUND_GENERIC + nDeathSoundOffset ) ); + } + + // Play an additional sound when we're in MvM and have a boss death + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && IsMiniBoss() ) + { + switch ( GetPlayerClass()->GetClassIndex() ) + { + case TF_CLASS_HEAVYWEAPONS: + { + EmitSound( "MVM.GiantHeavyExplodes" ); + break; + } + default: + { + EmitSound( "MVM.GiantCommonExplodes" ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const char* CTFPlayer::GetSceneSoundToken( void ) +{ + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + if ( IsMiniBoss() ) + { + return "M_MVM_"; + } + else + { + return "MVM_"; + } + } + else + { + return ""; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::StunSound( CTFPlayer* pAttacker, int iStunFlags, int iOldStunFlags ) +{ + if ( !IsAlive() ) + return; + + if ( !(iStunFlags & TF_STUN_CONTROLS) && !(iStunFlags & TF_STUN_LOSER_STATE) ) + return; + + if ( (iStunFlags & TF_STUN_BY_TRIGGER) && (iOldStunFlags != 0) ) + return; // Only play stun triggered sounds when not already stunned. + + // Play the stun sound for everyone but the attacker. + CMultiplayer_Expresser *pExpresser = GetMultiplayerExpresser(); + Assert( pExpresser ); + + pExpresser->AllowMultipleScenes(); + + float flStunSoundLength = 0; + EmitSound_t params; + params.m_flSoundTime = 0; + if ( iStunFlags & TF_STUN_SPECIAL_SOUND ) + { + params.m_pSoundName = "TFPlayer.StunImpactRange"; + } + else if ( (iStunFlags & TF_STUN_LOSER_STATE) && !pAttacker ) + { + params.m_pSoundName = "Halloween.PlayerScream"; + } + else + { + params.m_pSoundName = "TFPlayer.StunImpact"; + } + params.m_pflSoundDuration = &flStunSoundLength; + + if ( pAttacker ) + { + CPASFilter filter( GetAbsOrigin() ); + filter.RemoveRecipient( pAttacker ); + EmitSound( filter, entindex(), params ); + + // Play a louder pain sound for the person who got the stun. + CSingleUserRecipientFilter attackerFilter( pAttacker ); + EmitSound( attackerFilter, pAttacker->entindex(), params ); + } + else + { + EmitSound( params.m_pSoundName ); + } + + pExpresser->DisallowMultipleScenes(); + + // Suppress any pain sound that might come right after this stun sound. + m_flNextPainSoundTime = gpGlobals->curtime + 2.0f; +} + +//----------------------------------------------------------------------------- +// Purpose: called when this player burns another player +//----------------------------------------------------------------------------- +void CTFPlayer::OnBurnOther( CTFPlayer *pTFPlayerVictim, CTFWeaponBase *pWeapon ) +{ +#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 ) + { + AwardAchievement( ACHIEVEMENT_TF_BURN_PLAYERSINMINIMIMTIME ); + } + + // ACHIEVEMENT_TF_PYRO_KILL_SPIES - Awarded for igniting enemy spies who have active sappers on friendly building + if ( pTFPlayerVictim->IsPlayerClass(TF_CLASS_SPY)) + { + CBaseObject *pSapper = pTFPlayerVictim->GetObjectOfType( OBJ_ATTACHMENT_SAPPER, 0 ); + if ( pSapper ) + { + AwardAchievement( ACHIEVEMENT_TF_PYRO_KILL_SPIES ); + } + } + + // ACHIEVEMENT_TF_PYRO_BURN_RJ_SOLDIER - Pyro ignited a rocket jumping soldier in mid-air + if ( pTFPlayerVictim->IsPlayerClass(TF_CLASS_SOLDIER) ) + { + if ( pTFPlayerVictim->RocketJumped() && !pTFPlayerVictim->GetGroundEntity() ) + { + AwardAchievement( ACHIEVEMENT_TF_PYRO_BURN_RJ_SOLDIER ); + } + } + + // ACHIEVEMENT_TF_PYRO_DEFEND_POINTS - Pyro kills targets capping control points + CTriggerAreaCapture *pAreaTrigger = pTFPlayerVictim->GetControlPointStandingOn(); + if ( pAreaTrigger ) + { + CTeamControlPoint *pCP = pAreaTrigger->GetControlPoint(); + if ( pCP && pCP->GetOwner() == GetTeamNumber() ) + { + if ( TeamplayGameRules()->TeamMayCapturePoint( pTFPlayerVictim->GetTeamNumber(), pCP->GetPointIndex() ) && + TeamplayGameRules()->PlayerMayCapturePoint( pTFPlayerVictim, pCP->GetPointIndex() ) ) + { + AwardAchievement( ACHIEVEMENT_TF_PYRO_DEFEND_POINTS ); + } + } + } + + // ACHIEVEMENT_TF_MEDIC_ASSIST_PYRO + // if we're invuln, let the medic know that we burned someone + if ( m_Shared.InCond( TF_COND_INVULNERABLE ) || m_Shared.InCond( TF_COND_INVULNERABLE_WEARINGOFF ) ) + { + int i; + int iNumHealers = m_Shared.GetNumHealers(); + + for ( i=0;i<iNumHealers;i++ ) + { + // Send a message to all medics invulning the Pyro at this time + CTFPlayer *pMedic = ToTFPlayer( m_Shared.GetHealerByIndex( i ) ); + if ( pMedic && pMedic->GetChargeEffectBeingProvided() == MEDIGUN_CHARGE_INVULN ) + { + // Tell the clients involved in the ignition + CSingleUserRecipientFilter medic_filter( pMedic ); + UserMessageBegin( medic_filter, "PlayerIgnitedInv" ); + WRITE_BYTE( entindex() ); + WRITE_BYTE( pTFPlayerVictim->entindex() ); + WRITE_BYTE( pMedic->entindex() ); + MessageEnd(); + } + } + } + + // Tell the clients involved in the ignition + CRecipientFilter involved_filter; + involved_filter.AddRecipient( this ); + involved_filter.AddRecipient( pTFPlayerVictim ); + UserMessageBegin( involved_filter, "PlayerIgnited" ); + WRITE_BYTE( entindex() ); + WRITE_BYTE( pTFPlayerVictim->entindex() ); + WRITE_BYTE( pWeapon ? pWeapon->GetWeaponID() : 0 ); + MessageEnd(); + + IGameEvent *event = gameeventmanager->CreateEvent( "player_ignited" ); + if ( event ) + { + event->SetInt( "pyro_entindex", entindex() ); + event->SetInt( "victim_entindex", pTFPlayerVictim->entindex() ); + event->SetInt( "weaponid", pWeapon ? pWeapon->GetWeaponID() : 0 ); + gameeventmanager->FireEvent( event, true ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Returns true if the player is capturing a point. +//----------------------------------------------------------------------------- +bool CTFPlayer::IsCapturingPoint() +{ + CTriggerAreaCapture *pAreaTrigger = GetControlPointStandingOn(); + if ( pAreaTrigger ) + { + CTeamControlPoint *pCP = pAreaTrigger->GetControlPoint(); + if ( pCP ) + { + if ( TeamplayGameRules()->TeamMayCapturePoint( GetTeamNumber(), pCP->GetPointIndex() ) && + TeamplayGameRules()->PlayerMayCapturePoint( this, pCP->GetPointIndex() ) ) + { + // if we own this point, we're no longer "capturing" it + return pCP->GetOwner() != GetTeamNumber(); + } + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTFTeam *CTFPlayer::GetTFTeam( void ) +{ + CTFTeam *pTeam = dynamic_cast<CTFTeam *>( GetTeam() ); + Assert( pTeam ); + return pTeam; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTFTeam *CTFPlayer::GetOpposingTFTeam( void ) +{ + if ( TFTeamMgr() ) + { + int iTeam = GetTeamNumber(); + if ( iTeam == TF_TEAM_RED ) + { + return TFTeamMgr()->GetTeam( TF_TEAM_BLUE ); + } + else if ( iTeam == TF_TEAM_BLUE ) + { + return TFTeamMgr()->GetTeam( TF_TEAM_RED ); + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Give this player the "i just teleported" effect for 12 seconds +//----------------------------------------------------------------------------- +void CTFPlayer::TeleportEffect( void ) +{ + m_Shared.AddCond( TF_COND_TELEPORTED ); + + float flDuration = 12.f; + if ( TFGameRules()->IsMannVsMachineMode() && m_bIsABot && IsBotOfType( TF_BOT_TYPE ) ) + { + flDuration = 30.f; + } + + // Also removed on death + SetContextThink( &CTFPlayer::RemoveTeleportEffect, gpGlobals->curtime + flDuration, "TFPlayer_TeleportEffect" ); +} + +//----------------------------------------------------------------------------- +// Purpose: Remove the teleporter effect +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveTeleportEffect( void ) +{ + m_Shared.RemoveCond( TF_COND_TELEPORTED ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::StopRagdollDeathAnim( void ) +{ + CTFRagdoll *pRagdoll = dynamic_cast<CTFRagdoll*>( m_hRagdoll.Get() ); + if ( pRagdoll ) + { + pRagdoll->m_iDamageCustom = 0; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::CreateRagdollEntity( void ) +{ + CreateRagdollEntity( false, false, false, false, false, false, false, false ); +} + +//----------------------------------------------------------------------------- +// Purpose: Create a ragdoll entity to pass to the client. +//----------------------------------------------------------------------------- +void CTFPlayer::CreateRagdollEntity( bool bGib, bool bBurning, bool bElectrocuted, bool bOnGround, bool bCloakedCorpse, bool bGoldRagdoll, bool bIceRagdoll, bool bBecomeAsh, int iDamageCustom, bool bCritOnHardHit ) +{ + // If we already have a ragdoll destroy it. + CTFRagdoll *pRagdoll = dynamic_cast<CTFRagdoll*>( m_hRagdoll.Get() ); + if( pRagdoll ) + { + UTIL_Remove( pRagdoll ); + pRagdoll = NULL; + } + Assert( pRagdoll == NULL ); + + // Create a ragdoll. + pRagdoll = dynamic_cast<CTFRagdoll*>( CreateEntityByName( "tf_ragdoll" ) ); + if ( pRagdoll ) + { + pRagdoll->m_vecRagdollOrigin = GetAbsOrigin(); + pRagdoll->m_vecRagdollVelocity = GetAbsVelocity(); + pRagdoll->m_vecForce = m_vecForce; + 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_bElectrocuted = bElectrocuted; + pRagdoll->m_bOnGround = bOnGround; + pRagdoll->m_bCloaked = bCloakedCorpse; + pRagdoll->m_iDamageCustom = iDamageCustom; + pRagdoll->m_iTeam = GetTeamNumber(); + pRagdoll->m_iClass = GetPlayerClass()->GetClassIndex(); + pRagdoll->m_bGoldRagdoll = bGoldRagdoll; + pRagdoll->m_bIceRagdoll = bIceRagdoll; + pRagdoll->m_bBecomeAsh = bBecomeAsh; + pRagdoll->m_bCritOnHardHit = bCritOnHardHit; + pRagdoll->m_flHeadScale = m_flHeadScale; + pRagdoll->m_flTorsoScale = m_flTorsoScale; + pRagdoll->m_flHandScale = m_flHandScale; + } + + // Turn off the player. + AddSolidFlags( FSOLID_NOT_SOLID ); + AddEffects( EF_NODRAW | EF_NOSHADOW ); + SetMoveType( MOVETYPE_NONE ); + + // Add additional gib setup. + if ( bGib ) + { + m_nRenderFX = kRenderFxRagdoll; + } + + // Save ragdoll handle. + m_hRagdoll = pRagdoll; +} + +//----------------------------------------------------------------------------- +// Purpose: Destroy's a ragdoll, called with a player is disconnecting. +//----------------------------------------------------------------------------- +void CTFPlayer::DestroyRagdoll( void ) +{ + CTFRagdoll *pRagdoll = dynamic_cast<CTFRagdoll*>( m_hRagdoll.Get() ); + if( pRagdoll ) + { + UTIL_Remove( pRagdoll ); + } + + // Remove the feign death ragdoll at the same time. + pRagdoll = dynamic_cast<CTFRagdoll*>( m_hFeignRagdoll.Get() ); + if( pRagdoll ) + { + UTIL_Remove( pRagdoll ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: The player appears to die, creating a corpse and silently stealthing. +// Occurs when a player takes damage with the dead ringer active +//----------------------------------------------------------------------------- +void CTFPlayer::SpyDeadRingerDeath( const CTakeDamageInfo& info ) +{ + // Can't feign death if we're actually dead or if we're not a spy. + if ( !IsAlive() || !IsPlayerClass( TF_CLASS_SPY ) ) + return; + + // Can't feign death if we're already stealthed. + if ( m_Shared.InCond( TF_COND_STEALTHED ) ) + return; + + // Can't feign death if we aren't at full cloak energy. + if ( !CanGoInvisible( true ) || ( m_Shared.GetSpyCloakMeter() < 100.0f ) ) + return; + + m_Shared.SetSpyCloakMeter( 50.0f ); + + m_bGoingFeignDeath = true; + + FeignDeath( info ); + + // Go feign death. + m_Shared.AddCond( TF_COND_FEIGN_DEATH, tf_feign_death_duration.GetFloat() ); + m_bGoingFeignDeath = false; +} + +//----------------------------------------------------------------------------- +// Purpose: The player appears to die, creating a corpse +//----------------------------------------------------------------------------- +void CTFPlayer::FeignDeath( const CTakeDamageInfo& info ) +{ + if ( HasTheFlag() ) + { + DropFlag(); + } + + // Dead Ringer death removes Powerup Rune for authenticity + DropRune(); + + // Only drop disguised ragdoll & weapon if we're disguised as a teammate. + bool bDisguised = m_Shared.InCond( TF_COND_DISGUISED ) && (m_Shared.GetDisguiseTeam() == GetTeamNumber()); + + // We want the ragdoll to burn if the player was burning and was not disguised as a pyro. + bool bBurning = m_Shared.InCond( TF_COND_BURNING ) && (!bDisguised || (TF_CLASS_PYRO != m_Shared.GetDisguiseClass())); + + // Stop us from burning and other effects that would give the game away. + m_Shared.RemoveCond( TF_COND_BURNING ); + m_Shared.RemoveCond( TF_COND_BLEEDING ); + RemoveTeleportEffect(); + + // Fake death audio. + EmitSound( "BaseCombatCharacter.StopWeaponSounds" ); + SpeakConceptIfAllowed( MP_CONCEPT_DIED ); + DeathSound( info ); + + // Check if we should create gibs. + bool bGib = ShouldGib( info ); + + SetGibbedOnLastDeath( bGib ); + + // Fake death notice. + TFGameRules()->DeathNotice( this, info ); + + // Drop an empty ammo pack! + if ( ShouldDropAmmoPack() ) + { + DropAmmoPack( info, true /*Empty*/, bDisguised ); + } + + if ( TFGameRules()->IsInMedievalMode() ) + { + DropHealthPack( info, true ); + } + + if ( GetActiveTFWeapon() ) + { + int iDropHealthOnKill = 0; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetActiveTFWeapon(), iDropHealthOnKill, drop_health_pack_on_kill ); + if ( iDropHealthOnKill == 1 ) + { + DropHealthPack( info, true ); + } + } + + CTFPlayer *pTFPlayer = ToTFPlayer( info.GetAttacker() ); + if ( pTFPlayer ) + { + int iKillForcesAttackerToLaugh = 0; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFPlayer, iKillForcesAttackerToLaugh, kill_forces_attacker_to_laugh ); + if ( iKillForcesAttackerToLaugh == 1 ) + { + // force the attacker to laugh! + pTFPlayer->Taunt( TAUNT_MISC_ITEM, MP_CONCEPT_TAUNT_LAUGH ); + } + + CTFWeaponInvis *pWpn = (CTFWeaponInvis *)Weapon_OwnsThisID( TF_WEAPON_INVIS ); + if ( pWpn && pWpn->HasFeignDeath() ) + { + DropDeathCallingCard( pTFPlayer, this ); + } + } + + // Create a ragdoll. + CreateFeignDeathRagdoll( info, bGib, bBurning, bDisguised ); + + // Note that we succeeded for stats tracking. + EconEntity_OnOwnerKillEaterEvent( dynamic_cast<CEconEntity *>( GetEntityForLoadoutSlot( LOADOUT_POSITION_PDA2 ) ), + this, + pTFPlayer, // in this case the "victim" is the person doing the damage + kKillEaterEvent_DeathsFeigned ); +} + +//----------------------------------------------------------------------------- +// Purpose: Create a ragdoll entity for feign death. Does not hide the player. +// Creates an entirely seperate ragdoll that isn't used for client death cam or other real death stuff. +//----------------------------------------------------------------------------- +void CTFPlayer::CreateFeignDeathRagdoll( const CTakeDamageInfo& info, bool bGib, bool bBurning, bool bDisguised ) +{ + // If we already have a feigning ragdoll destroy it. + CTFRagdoll *pRagdoll = dynamic_cast<CTFRagdoll*>( m_hFeignRagdoll.Get() ); + if( pRagdoll ) + { + UTIL_Remove( pRagdoll ); + pRagdoll = NULL; + } + Assert( pRagdoll == NULL ); + + // Create a ragdoll. + pRagdoll = dynamic_cast<CTFRagdoll*>( CreateEntityByName( "tf_ragdoll" ) ); + if ( pRagdoll ) + { + pRagdoll->m_vecRagdollOrigin = GetAbsOrigin(); + pRagdoll->m_vecRagdollVelocity = m_vecFeignDeathVelocity; + pRagdoll->m_vecForce = CalcDamageForceVector( info ); + 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_bElectrocuted = false; + pRagdoll->m_bFeignDeath = true; + pRagdoll->m_bWasDisguised = bDisguised; + pRagdoll->m_bBecomeAsh = false; + pRagdoll->m_bOnGround = (bool) (GetFlags() & FL_ONGROUND); + pRagdoll->m_iDamageCustom = info.GetDamageCustom(); + pRagdoll->m_bCritOnHardHit = false; + pRagdoll->m_flHeadScale = m_flHeadScale; + pRagdoll->m_flTorsoScale = m_flTorsoScale; + pRagdoll->m_flHandScale = m_flHandScale; + + { + int iGoldRagdoll = 0; + if ( info.GetWeapon() ) + { + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iGoldRagdoll, set_turn_to_gold ); + } + pRagdoll->m_bGoldRagdoll = iGoldRagdoll != 0; + + int iIceRagdoll = 0; + if ( info.GetWeapon() ) + { + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iIceRagdoll, set_turn_to_ice ); + } + pRagdoll->m_bIceRagdoll = iIceRagdoll != 0; + + int iRagdollsBecomeAsh = 0; + if ( info.GetWeapon() ) + { + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iRagdollsBecomeAsh, ragdolls_become_ash ); + } + pRagdoll->m_bBecomeAsh = iRagdollsBecomeAsh != 0; + + int iRagdollsPlasmaEffect = 0; + if ( info.GetWeapon() ) + { + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iRagdollsPlasmaEffect, ragdolls_plasma_effect ); + } + if ( iRagdollsPlasmaEffect ) + { + pRagdoll->m_iDamageCustom = TF_DMG_CUSTOM_PLASMA; + } + + int iCritOnHardHit = 0; + if ( info.GetWeapon() ) + { + CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iCritOnHardHit, crit_on_hard_hit ); + } + pRagdoll->m_bCritOnHardHit = iCritOnHardHit != 0; + } + + // If we are disguised, make the ragdoll look like our disguise. + if ( bDisguised ) + { + pRagdoll->m_iTeam = m_Shared.GetDisguiseTeam(); + pRagdoll->m_iClass = m_Shared.GetDisguiseClass(); + } + else + { + pRagdoll->m_iTeam = GetTeamNumber(); + pRagdoll->m_iClass = GetPlayerClass()->GetClassIndex(); + } + } + + // Exaggerate ragdoll velocity if recently hit by blast damage. + if ( !bGib && ( info.GetDamageType() & DMG_BLAST ) ) + { + Vector vForceModifier = info.GetDamageForce(); + vForceModifier.x *= 1.5; + vForceModifier.y *= 1.5; + vForceModifier.z *= 1; + pRagdoll->m_vecForce = vForceModifier; + } + + // Save ragdoll handle. + m_hFeignRagdoll = 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: Call this when this player fires a weapon to allow other systems to react +//----------------------------------------------------------------------------- +void CTFPlayer::OnMyWeaponFired( CBaseCombatWeapon *weapon ) +{ + BaseClass::OnMyWeaponFired( weapon ); + + // mark region as 'in combat' + if ( m_inCombatThrottleTimer.IsElapsed() ) + { + CTFWeaponBase *tfWeapon = static_cast< CTFWeaponBase * >( weapon ); + + if ( !tfWeapon ) + { + return; + } + + switch ( tfWeapon->GetWeaponID() ) + { + case TF_WEAPON_MEDIGUN: + case TF_WEAPON_PDA: + case TF_WEAPON_PDA_ENGINEER_BUILD: + case TF_WEAPON_PDA_ENGINEER_DESTROY: + case TF_WEAPON_PDA_SPY: + case TF_WEAPON_BUILDER: + case TF_WEAPON_DISPENSER: + case TF_WEAPON_INVIS: + case TF_WEAPON_LUNCHBOX: + case TF_WEAPON_BUFF_ITEM: + case TF_WEAPON_PUMPKIN_BOMB: + case TF_WEAPON_WRENCH: // skip this so engineer building doesn't mark 'in combat' + case TF_WEAPON_PDA_SPY_BUILD: + // not a 'combat' weapon + return; + }; + + // important to keep this at one second, so rate cvars make sense (units/sec) + m_inCombatThrottleTimer.Start( 1.0f ); + + // only search up/down StepHeight as a cheap substitute for line of sight + CUtlVector< CNavArea * > nearbyAreaVector; + CollectSurroundingAreas( &nearbyAreaVector, GetLastKnownArea(), tf_nav_in_combat_range.GetFloat(), StepHeight, StepHeight ); + + for( int i=0; i<nearbyAreaVector.Count(); ++i ) + { + static_cast< CTFNavArea * >( nearbyAreaVector[i] )->OnCombat(); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Remove invisibility, called when player attacks +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveInvisibility( void ) +{ + if ( !m_Shared.IsStealthed() ) + return; + + // remove quickly + CTFPlayer *pProvider = ToTFPlayer( m_Shared.GetConditionProvider( TF_COND_STEALTHED_USER_BUFF ) ); + bool bAEStealth = ( m_Shared.InCond( TF_COND_STEALTHED_USER_BUFF ) && + pProvider && + ( pProvider->IsPlayerClass( TF_CLASS_SPY ) ? true : false ) && + ( pProvider != this ) ); + if ( m_Shared.InCond( TF_COND_STEALTHED_USER_BUFF ) ) + { + m_Shared.AddCond( TF_COND_STEALTHED_USER_BUFF_FADING, ( bAEStealth ) ? 4.f : 0.5f ); + } + + m_Shared.FadeInvis( bAEStealth ? 2.f : 0.5f ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::SayAskForBall() +{ + if ( !TFGameRules() || !TFGameRules()->IsPasstimeMode() + || ( m_Shared.AskForBallTime() > gpGlobals->curtime ) ) + { + return false; + } + + CPasstimeBall *pBall = g_pPasstimeLogic->GetBall(); + if ( !pBall ) + { + return false; + } + + CTFPlayer *pBallCarrier = pBall->GetCarrier(); + if ( !pBallCarrier ) + { + return false; + } + + HudNotification_t cantCarryReason; + if ( !CPasstimeGun::BValidPassTarget( pBallCarrier, this, &cantCarryReason ) ) + { + if ( cantCarryReason ) + { + CSingleUserReliableRecipientFilter filter( this ); + TFGameRules()->SendHudNotification( filter, cantCarryReason ); + } + return false; + } + + CRecipientFilter filter; + filter.AddRecipient( this ); + filter.AddRecipient( pBallCarrier ); + filter.MakeReliable(); + EmitSound( filter, entindex(), "Passtime.AskForBall" ); + + ++CTF_GameStats.m_passtimeStats.summary.nTotalPassRequests; + m_Shared.SetAskForBallTime( gpGlobals->curtime + 5.0f ); + return true; +} + +//----------------------------------------------------------------------------- +// 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 ) + return; + + if ( pPlayer->m_Shared.IsCarryingRune() ) + { + pPlayer->DropRune(); + return; + } + + if ( pPlayer->HasTheFlag() ) + { + pPlayer->DropFlag(); + } +} +static ConCommand dropitem( "dropitem", CC_DropItem, "Drop the flag." ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CObserverPoint::CObserverPoint() +{ + m_bMatchSummary = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObserverPoint::Activate( void ) +{ + BaseClass::Activate(); + + if ( m_bMatchSummary ) + { + // sanity check to make sure the competitive match summary target is disabled until we're ready for it + SetDisabled( true ); + } + + 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) ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CObserverPoint::CanUseObserverPoint( CTFPlayer *pPlayer ) +{ + if ( m_bDisabled ) + return false; + + // Only spectate observer points on control points in the current miniround + if ( g_pObjectiveResource->PlayingMiniRounds() && m_hAssociatedTeamEntity ) + { + CTeamControlPoint *pPoint = dynamic_cast<CTeamControlPoint*>(m_hAssociatedTeamEntity.Get()); + if ( pPoint ) + { + bool bInRound = g_pObjectiveResource->IsInMiniRound( pPoint->GetPointIndex() ); + if ( !bInRound ) + return false; + } + } + + if ( m_hAssociatedTeamEntity && mp_forcecamera.GetInt() == OBS_ALLOW_TEAM ) + { + // don't care about this check during a team win + if ( TFGameRules() && TFGameRules()->State_Get() != GR_STATE_TEAM_WIN ) + { + // 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; + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CObserverPoint::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObserverPoint::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObserverPoint::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} + +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_KEYFIELD( m_bMatchSummary, FIELD_BOOLEAN, "match_summary" ), + +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 ); + } + } + } + +#ifdef TF_RAID_MODE + // Add all of the objects for my team if we're in Raid mode + if ( TFGameRules() && TFGameRules()->IsRaidMode() ) + { + CTFTeam *pTeam = TFTeamMgr()->GetTeam( TF_TEAM_BLUE ); + if ( pTeam ) + { + int nTeamObjectCount = pTeam->GetNumObjects(); + + for ( int iObject = 0; iObject < nTeamObjectCount; ++iObject ) + { + CBaseObject *pObj = pTeam->GetObject( iObject ); + + if ( !pObj ) + continue; + + // we've already added our own buildings in the previous loop + if ( pObj->GetOwner() == this ) + continue; + + m_hObservableEntities.AddToTail( pObj ); + + if ( m_hObserverTarget.Get() == pObj ) + { + iCurrentIndex = ( m_hObservableEntities.Count() - 1 ); + } + } + } + } +#endif // TF_RAID_MODE + + // If there are any team_train_watchers, add the train they are linked to + CTeamTrainWatcher *pWatcher = dynamic_cast<CTeamTrainWatcher*>( gEntList.FindEntityByClassname( NULL, "team_train_watcher" ) ); + while ( pWatcher ) + { + if ( !pWatcher->IsDisabled() ) + { + CBaseEntity *pTrain = pWatcher->GetTrainEntity(); + if ( pTrain ) + { + m_hObservableEntities.AddToTail( pTrain ); + + if ( m_hObserverTarget.Get() == pTrain ) + { + iCurrentIndex = (m_hObservableEntities.Count()-1); + } + } + } + + pWatcher = dynamic_cast<CTeamTrainWatcher*>( gEntList.FindEntityByClassname( pWatcher, "team_train_watcher" ) ); + } + + // observe active bosses + if ( TFGameRules()->GetActiveBoss() ) + { + m_hObservableEntities.AddToTail( TFGameRules()->GetActiveBoss() ); + + if ( m_hObserverTarget.Get() == TFGameRules()->GetActiveBoss() ) + { + 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 == this ) + return false; + + // if we are coaching, the target is always valid + if ( target && m_hStudent == target && target->IsPlayer() ) + { + return true; + } + + bool bAllowInTournament = false; + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + bAllowInTournament = true; + } + + if ( TFGameRules()->IsPasstimeMode() && (target == TFGameRules()->GetObjectiveObserverTarget()) ) + { + return true; + } + + if ( target && !target->IsPlayer() ) + { + //Can only spectate players in Tournament Mode + if ( TFGameRules()->IsInTournamentMode() == true && !bAllowInTournament ) + return false; + + CObserverPoint *pObsPoint = dynamic_cast<CObserverPoint *>(target); + if ( pObsPoint && !pObsPoint->CanUseObserverPoint( this ) ) + return false; + + CFuncTrackTrain *pTrain = dynamic_cast<CFuncTrackTrain *>(target); + if ( pTrain ) + { + // can only spec the trains while the round is running + if ( TFGameRules()->State_Get() == GR_STATE_TEAM_WIN ) + 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 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::PickWelcomeObserverPoint( void ) +{ + //Don't just spawn at the world origin, find a nice spot to look from while we choose our team and class. + CObserverPoint *pObserverPoint = (CObserverPoint *)gEntList.FindEntityByClassname( NULL, "info_observer_point" ); + + while ( pObserverPoint ) + { + if ( IsValidObserverTarget( pObserverPoint ) ) + { + SetObserverTarget( pObserverPoint ); + } + + if ( pObserverPoint->IsDefaultWelcome() ) + break; + + pObserverPoint = (CObserverPoint *)gEntList.FindEntityByClassname( pObserverPoint, "info_observer_point" ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::SetObserverTarget(CBaseEntity *target) +{ + ClearZoomOwner(); + SetFOV( this, 0 ); + + if ( !BaseClass::SetObserverTarget(target) ) + return false; + + CObserverPoint *pObsPoint = dynamic_cast<CObserverPoint *>(target); + if ( pObsPoint ) + { + SetViewOffset( vec3_origin ); + JumptoPosition( target->GetAbsOrigin(), target->EyeAngles() ); + SetFOV( pObsPoint, pObsPoint->m_flFOV ); + } + + if ( !m_bArenaIsAFK ) + { + 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 there is a Boss active, watch him + if ( TFGameRules()->GetActiveBoss() ) + { + m_hObserverTarget.Set( TFGameRules()->GetActiveBoss() ); + } + + // If we're on a team (i.e. not a pure observer), try and find + // a target that'll give the player the most useful information. + if ( GetTeamNumber() >= FIRST_GAME_TEAM ) + { + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster ) + { + // Has our forward cap point been contested recently? + int iFarthestPoint = TFGameRules()->GetFarthestOwnedControlPoint( GetTeamNumber(), false ); + if ( iFarthestPoint != -1 ) + { + float flTime = pMaster->PointLastContestedAt( iFarthestPoint ); + if ( flTime != -1 && flTime > (gpGlobals->curtime - 30) ) + { + // Does it have an associated viewpoint? + CBaseEntity *pObserverPoint = gEntList.FindEntityByClassname( NULL, "info_observer_point" ); + while ( pObserverPoint ) + { + CObserverPoint *pObsPoint = assert_cast<CObserverPoint *>(pObserverPoint); + if ( pObsPoint && pObsPoint->m_hAssociatedTeamEntity == pMaster->GetControlPoint(iFarthestPoint) ) + { + if ( IsValidObserverTarget( pObsPoint ) ) + { + m_hObserverTarget.Set( pObsPoint ); + return; + } + } + + pObserverPoint = gEntList.FindEntityByClassname( pObserverPoint, "info_observer_point" ); + } + } + } + + // Has the point beyond our farthest been contested lately? + iFarthestPoint += (ObjectiveResource()->GetBaseControlPointForTeam( GetTeamNumber() ) == 0 ? 1 : -1); + if ( iFarthestPoint >= 0 && iFarthestPoint < MAX_CONTROL_POINTS ) + { + float flTime = pMaster->PointLastContestedAt( iFarthestPoint ); + if ( flTime != -1 && flTime > (gpGlobals->curtime - 30) ) + { + // Try and find a player near that cap point + CBaseEntity *pCapPoint = pMaster->GetControlPoint(iFarthestPoint); + if ( pCapPoint ) + { + CBaseEntity *pTarget = FindNearestObservableTarget( pCapPoint->GetAbsOrigin(), 1500 ); + if ( pTarget ) + { + m_hObserverTarget.Set( pTarget ); + return; + } + } + } + } + } + } + + // Find the nearest guy near myself + CBaseEntity *pTarget = FindNearestObservableTarget( GetAbsOrigin(), FLT_MAX ); + if ( pTarget ) + { + m_hObserverTarget.Set( pTarget ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ValidateCurrentObserverTarget( void ) +{ + // If our current target is a dead player who's gibbed / died, refind as if + // we were finding our initial target, so we end up somewhere useful. + if ( m_hObserverTarget && m_hObserverTarget->IsPlayer() ) + { + CBasePlayer *player = ToBasePlayer( m_hObserverTarget ); + + if ( player->m_lifeState == LIFE_DEAD || player->m_lifeState == LIFE_DYING ) + { + // if we are coaching, don't switch + if ( m_hStudent == m_hObserverTarget ) + { + return; + } + + // 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->IsPlayer() ) + { + // can only spectate players in-eye + if ( m_iObserverMode == OBS_MODE_IN_EYE ) + { + ForceObserverMode( OBS_MODE_CHASE ); + } + } + + BaseClass::ValidateCurrentObserverTarget(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::CheckObserverSettings() +{ + // make sure we are always observing the student + if ( m_hObserverTarget && m_hStudent && m_hStudent != m_hObserverTarget ) + { + SetObserverTarget( m_hStudent ); + } + else if ( TFGameRules() ) + { + // is there a current entity that is the required spectator target? + if ( TFGameRules()->GetRequiredObserverTarget() ) + { + SetObserverTarget( TFGameRules()->GetRequiredObserverTarget() ); + return; + } + + if ( TFGameRules()->IsPasstimeMode() && g_pPasstimeLogic && (GetObserverMode() == OBS_MODE_POI) ) + { + CPasstimeBall *pBall = g_pPasstimeLogic->GetBall(); + if ( !pBall || ((m_hObserverTarget.Get() == pBall) && pBall->BOutOfPlay()) ) + { + FindInitialObserverTarget(); + } + else if ( !pBall->BOutOfPlay() && (GetObserverTarget() != TFGameRules()->GetObjectiveObserverTarget()) ) + { + SetObserverTarget( TFGameRules()->GetObjectiveObserverTarget() ); + } + return; + } + + // make sure we're not trying to spec the train during a team win + // if we are, switch to spectating the last control point instead (where the train ended) + if ( m_hObserverTarget && m_hObserverTarget->IsBaseTrain() && TFGameRules()->State_Get() == GR_STATE_TEAM_WIN ) + { + // find the nearest spectator point to use instead of the train + CObserverPoint *pObserverPoint = (CObserverPoint *)gEntList.FindEntityByClassname( NULL, "info_observer_point" ); + CObserverPoint *pClosestPoint = NULL; + float flMinDistance = -1.0f; + Vector vecTrainOrigin = m_hObserverTarget->GetAbsOrigin(); + + while ( pObserverPoint ) + { + if ( IsValidObserverTarget( pObserverPoint ) ) + { + float flDist = pObserverPoint->GetAbsOrigin().DistTo( vecTrainOrigin ); + if ( flMinDistance < 0 || flDist < flMinDistance ) + { + flMinDistance = flDist; + pClosestPoint = pObserverPoint; + } + } + + pObserverPoint = (CObserverPoint *)gEntList.FindEntityByClassname( pObserverPoint, "info_observer_point" ); + } + + if ( pClosestPoint ) + { + SetObserverTarget( pClosestPoint ); + } + } + } + + BaseClass::CheckObserverSettings(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::Touch( CBaseEntity *pOther ) +{ + CTFPlayer *pVictim = ToTFPlayer( pOther ); + + if ( pVictim ) + { + // ACHIEVEMENT_TF_SPY_BUMP_CLOAKED_SPY + if ( !m_Shared.IsAlly( pVictim ) ) + { + if ( IsPlayerClass( TF_CLASS_SPY ) && pVictim->IsPlayerClass( TF_CLASS_SPY ) ) + { + if ( m_Shared.InCond( TF_COND_STEALTHED ) && pVictim->m_Shared.InCond( TF_COND_STEALTHED ) ) + { + AwardAchievement( ACHIEVEMENT_TF_SPY_BUMP_CLOAKED_SPY ); + } + } + } + + CheckUncoveringSpies( pVictim ); + + // ACHIEVEMENT_TF_HEAVY_BLOCK_INVULN_HEAVY + if ( !m_Shared.IsAlly( pVictim ) ) + { + if ( IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && pVictim->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ) + { + CTFTeam *pTeam = GetGlobalTFTeam( GetTeamNumber() ); + if ( pTeam && pTeam->GetRole() == TEAM_ROLE_DEFENDERS ) + { + if ( m_Shared.InCond( TF_COND_INVULNERABLE ) || m_Shared.InCond( TF_COND_INVULNERABLE_WEARINGOFF ) ) + { + if ( pVictim->m_Shared.InCond( TF_COND_INVULNERABLE ) || pVictim->m_Shared.InCond( TF_COND_INVULNERABLE_WEARINGOFF ) ) + { + float flMaxSpeed = 50.0f * 50.0f; + if ( ( GetAbsVelocity().LengthSqr() < flMaxSpeed ) && ( pVictim->GetAbsVelocity().LengthSqr() < flMaxSpeed ) ) + { + AwardAchievement( ACHIEVEMENT_TF_HEAVY_BLOCK_INVULN_HEAVY ); + } + } + } + } + } + + // **************************************************************************************************************** + // Halloween Karts + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) && m_flHalloweenKartPushEventTime < gpGlobals->curtime ) + { + // calculate a force and save it off, it is used on a later frame cause it is to late to apply the force here + float flImpactForce = GetLocalVelocity().Length(); + if ( flImpactForce > 10.0f ) + { + float flForceMult = 1.0f; + + Vector vAim = GetLocalVelocity(); + vAim.NormalizeInPlace(); + Vector vOrigin = GetAbsOrigin(); + + // Force direction is velocity of the player in the case that this is a head on collison. + // Trace + trace_t pTrace; + Ray_t ray; + CTraceFilterOnlyNPCsAndPlayer pFilter( this, COLLISION_GROUP_NONE ); + //tf_halloween_kart_impact_lookahead + //tf_halloween_kart_impact_bounds_scale + ray.Init( vOrigin, Vector( 0, 0, 16 ) + vOrigin + vAim * tf_halloween_kart_impact_lookahead.GetFloat(), GetPlayerMins() * tf_halloween_kart_impact_bounds_scale.GetFloat(), GetPlayerMaxs() * tf_halloween_kart_impact_bounds_scale.GetFloat() ); + enginetrace->TraceRay( ray, MASK_SOLID, &pFilter, &pTrace ); + + Vector vecForceDirection; + vecForceDirection = vAim; + vecForceDirection.z += 0.60f; + vecForceDirection.NormalizeInPlace(); + if ( pTrace.m_pEnt == pVictim ) + { + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART_DASH ) ) + { + flForceMult *= tf_halloween_kart_boost_impact_force.GetFloat(); + // Stop moving + SetAbsVelocity( vec3_origin ); + SetCurrentTauntMoveSpeed( 0 ); + m_Shared.RemoveCond( TF_COND_HALLOWEEN_KART_DASH ); + EmitSound( "BumperCar.BumpHard" ); + } + else + { + SetAbsVelocity( GetAbsVelocity() * tf_halloween_kart_impact_feedback.GetFloat() ); + SetCurrentTauntMoveSpeed( GetCurrentTauntMoveSpeed() * tf_halloween_kart_impact_feedback.GetFloat() ); + EmitSound( "BumperCar.Bump" ); + } + + // Invul Crash + if ( m_Shared.InCond( TF_COND_INVULNERABLE_USER_BUFF ) ) + { + flForceMult += 0.5f; + } + + // Apply some kart damage + //Speed maxes at 800, normally at 300? we want about 10 damage a hit? 10-15? + int iDamage = (int)( ( flImpactForce / 50.0f + RandomInt( 13, 19 ) ) * tf_halloween_kart_impact_damage.GetFloat() ); + + // Apply force to enemy + vecForceDirection *= flImpactForce * flForceMult * tf_halloween_kart_impact_force.GetFloat(); + pVictim->AddHalloweenKartPushEvent( this, NULL, NULL, vecForceDirection, iDamage ); + } + else + { + DevMsg( "Collision with player not in Trace, %f Force \n", flImpactForce ); + } + + // can only give a kart push event every 0.2 seconds + if ( vecForceDirection.LengthSqr() > 100.0f ) + { + m_flHalloweenKartPushEventTime = gpGlobals->curtime + tf_halloween_kart_impact_rate.GetFloat(); + } + } + } + if ( ( m_Shared.GetPercentInvisible() < 0.10f ) && + m_Shared.GetCarryingRuneType() == RUNE_PLAGUE && + !m_Shared.IsAlly( pVictim ) && + !pVictim->m_Shared.IsInvulnerable() && + !pVictim->m_Shared.InCond( TF_COND_PLAGUE ) && + pVictim->m_Shared.GetCarryingRuneType() != RUNE_RESIST ) + { + pVictim->m_Shared.AddCond( TF_COND_PLAGUE, PERMANENT_CONDITION, this ); + + //Plague transmission event infects nearby eligible players on the same team. Only works for powerup carrier to host, not host to host. + const Vector& vecPos = pVictim->WorldSpaceCenter(); + for ( int i = 0; i < pVictim->GetTeam()->GetNumPlayers(); i++ ) + { + CTFPlayer *pTeamMate = ToTFPlayer( pVictim->GetTeam()->GetPlayer( i ) ); + + if ( pTeamMate && pTeamMate != pVictim && pTeamMate->IsAlive() && !pTeamMate->m_Shared.IsInvulnerable() && !pTeamMate->m_Shared.InCond( TF_COND_PLAGUE ) && pTeamMate->m_Shared.GetCarryingRuneType() != RUNE_RESIST ) + { + // Only nearby teammates. Check for this before the more expensive visibility trace + if ( ( vecPos - pTeamMate->WorldSpaceCenter() ).LengthSqr() < ( 350 * 350 ) ) + { + // Doesn't go through walls + if ( pVictim->FVisible( pTeamMate, MASK_SOLID ) ) + { + pTeamMate->m_Shared.AddCond( TF_COND_PLAGUE, PERMANENT_CONDITION, this ); + CPVSFilter filter( WorldSpaceCenter() ); + Vector vStart = pVictim->EyePosition(); + Vector vEnd = pTeamMate->GetAbsOrigin() + Vector( 0, 0, 56 ); + te_tf_particle_effects_control_point_t controlPoint = { PATTACH_ABSORIGIN, vEnd }; + TE_TFParticleEffectComplex( filter, 0.f, "plague_transmission", vStart, QAngle( 0.f, 0.f, 0.f ), NULL, &controlPoint, pTeamMate, PATTACH_CUSTOMORIGIN ); + } + } + } + } + } + } + } + + BaseClass::Touch( pOther ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::RefreshCollisionBounds( void ) +{ + BaseClass::RefreshCollisionBounds(); + + SetViewOffset( ( IsDucked() ) ? ( VEC_DUCK_VIEW_SCALED( this ) ) : ( GetClassEyeHeight() ) ); +} + +//----------------------------------------------------------------------------- +/** + * Invoked (by UpdateLastKnownArea) when we enter a new nav area (or it is reset to NULL) + */ +void CTFPlayer::OnNavAreaChanged( CNavArea *enteredArea, CNavArea *leftArea ) +{ + VPROF_BUDGET( "CTFPlayer::OnNavAreaChanged", "NextBot" ); + + if ( !IsAlive() || GetTeamNumber() == TEAM_SPECTATOR ) + { + return; + } + + if ( leftArea ) + { + // remove us from old visible set + NavAreaCollector wasVisible; + leftArea->ForAllPotentiallyVisibleAreas( wasVisible ); + + for( int i=0; i<wasVisible.m_area.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)wasVisible.m_area[i]; + area->RemovePotentiallyVisibleActor( this ); + } + } + + + if ( enteredArea ) + { + // add us to new visible set + // @todo: is it faster to only do this for the areas that changed between sets? + NavAreaCollector isVisible; + enteredArea->ForAllPotentiallyVisibleAreas( isVisible ); + + for( int i=0; i<isVisible.m_area.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)isVisible.m_area[i]; + area->AddPotentiallyVisibleActor( this ); + } + } +} + +//----------------------------------------------------------------------------------------------------- +// Return true if the given threat is aiming in our direction +bool CTFPlayer::IsThreatAimingTowardMe( CBaseEntity *threat, float cosTolerance ) const +{ + CTFPlayer *player = ToTFPlayer( threat ); + Vector to = GetAbsOrigin() - threat->GetAbsOrigin(); + float threatRange = to.NormalizeInPlace(); + Vector forward; + + if ( player == NULL ) + { + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( threat ); + if ( sentry ) + { + // are we in range? + if ( threatRange < SENTRY_MAX_RANGE ) + { + // is it pointing at us? + AngleVectors( sentry->GetTurretAngles(), &forward ); + + if ( DotProduct( to, forward ) > cosTolerance ) + { + return true; + } + } + } + + // not a player, not a sentry, not a threat? + return false; + } + + // is the player pointing at me? + player->EyeVectors( &forward ); + + if ( DotProduct( to, forward ) > cosTolerance ) + { + return true; + } + + return false; +} + +//----------------------------------------------------------------------------------------------------- +// Return true if the given threat is aiming in our direction and firing its weapon +bool CTFPlayer::IsThreatFiringAtMe( CBaseEntity *threat ) const +{ + if ( IsThreatAimingTowardMe( threat ) ) + { + CTFPlayer *player = ToTFPlayer( threat ); + + if ( player ) + { + return player->IsFiringWeapon(); + } + + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( threat ); + if ( sentry ) + { + return sentry->GetTimeSinceLastFired() < 1.0f; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// 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::DoNoiseMaker( void ) +{ + if ( gpGlobals->curtime < m_Shared.GetNextNoiseMakerTime() ) + return; + + CSteamID steamIDForPlayer; + GetSteamID( &steamIDForPlayer ); + + // Check to see that we have a noise maker item equipped. We intentionally + // want to check this to fix the infinite noise maker bugs. + CEconItemView *pItem = GetEquippedItemForLoadoutSlot( LOADOUT_POSITION_ACTION ); + if ( !pItem ) + return; + + int iUnlimitedQuantity = 0; + CALL_ATTRIB_HOOK_INT( iUnlimitedQuantity, unlimited_quantity ); + + if ( pItem->GetItemQuantity() <= 0 && !iUnlimitedQuantity ) + return; + + perteamvisuals_t* vis = pItem->GetStaticData()->GetPerTeamVisual( 0 ); + if ( !vis ) + return; + + int iNumSounds = 0; + for ( int i=0; i<MAX_VISUALS_CUSTOM_SOUNDS; ++i ) + { + if ( vis->pszCustomSounds[i] ) + iNumSounds++; + } + + if ( iNumSounds == 0 ) + return; + + int rand = RandomInt( 0, iNumSounds-1 ); + + float flSoundLength = 0; + EmitSound_t params; + params.m_flSoundTime = 0; + params.m_pSoundName = vis->pszCustomSounds[rand]; + params.m_pflSoundDuration = &flSoundLength; + + CPASFilter filter( GetAbsOrigin() ); + EmitSound( filter, entindex(), params ); + + // Add a particle effect. + const char *particleEffectName = pItem->GetStaticData()->GetParticleEffect( TEAM_UNASSIGNED ); + if ( particleEffectName ) + { + TE_TFParticleEffect( filter, 0.0, particleEffectName, PATTACH_POINT_FOLLOW, this, "head" ); + } + + float flDelay = 1.0f; + + // Duck Badge Cooldown is based on badge level. Noisemaker is more like an easter egg + CSchemaAttributeDefHandle pAttr_DuckLevelBadge( "duck badge level" ); + uint32 iDuckBadgeLevel = 0; + + if ( FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pItem, pAttr_DuckLevelBadge, &iDuckBadgeLevel ) ) + { + flDelay = 5.0f; + } + + // Throttle the usage rate to sound duration plus some dead time. + m_Shared.SetNextNoiseMakerTime( gpGlobals->curtime + flSoundLength + flDelay ); +} + +//----------------------------------------------------------------------------- +// Purpose: Finds an open space for a high five partner. flTolerance specifies the maximum amount that should be allowed underneath position. +//----------------------------------------------------------------------------- +bool CTFPlayer::FindOpenTauntPartnerPosition( CEconItemView *pEconItemView, Vector &position, float *flTolerance ) +{ + if ( !pEconItemView || !pEconItemView->IsValid() ) + return false; + + const GameItemDefinition_t *pItemDef = pEconItemView->GetItemDefinition(); + if ( !pItemDef || !pItemDef->GetTauntData() ) + { + position = GetAbsOrigin(); + *flTolerance = tf_highfive_height_tolerance.GetFloat(); + return false; + } + + const float flTauntSeparationForwardDistance = tf_highfive_separation_forward.GetFloat() != 0 ? tf_highfive_separation_forward.GetFloat() : pItemDef->GetTauntData()->GetTauntSeparationForwardDistance(); + const float flTauntSeparationRightDistance = tf_highfive_separation_right.GetFloat() != 0 ? tf_highfive_separation_right.GetFloat() : pItemDef->GetTauntData()->GetTauntSeparationRightDistance(); + + bool ret = true; + Vector forward, right; + AngleVectors( GetAbsAngles(), &forward, &right, NULL ); + + Vector vecStart = GetAbsOrigin(); + Vector vecEnd = vecStart + ( forward * flTauntSeparationForwardDistance ) + ( right * flTauntSeparationRightDistance ); + *flTolerance = tf_highfive_height_tolerance.GetFloat(); + trace_t result; + CTraceFilterIgnoreTeammates filter( this, COLLISION_GROUP_NONE, GetAllowedTauntPartnerTeam() ); + UTIL_TraceHull( vecStart, vecEnd + ( forward * 2 ), VEC_HULL_MIN, VEC_HULL_MAX, MASK_PLAYERSOLID, &filter, &result ); + + if ( result.DidHit() ) + { + // something's directly in front of us, but let's allow for a little bit of variation since we might be standing on an uneven displacement + trace_t result2; + vecStart = GetAbsOrigin() + Vector( 0, 0, *flTolerance ); + vecEnd = vecStart + ( forward * flTauntSeparationForwardDistance ) + ( right * flTauntSeparationRightDistance ); + UTIL_TraceHull( vecStart, vecEnd + ( forward * 2 ), VEC_HULL_MIN, VEC_HULL_MAX, MASK_PLAYERSOLID, &filter, &result2 ); + + // Now we can allow for twice the space underneath us. + *flTolerance *= 2; + + if ( result2.DidHit() ) + { + // Not enough space in front of us. + ret = false; + } + else + { + position = vecEnd; + } + } + else + { + position = vecEnd; + } + + if( ret ) + { + Vector vecStartCenter = WorldSpaceCenter(); + // Scale up how far we test. Dont even let them get close. + Vector vecEndSuperSafe = vecStartCenter + ( forward * flTauntSeparationForwardDistance * 2.f ) + ( right * flTauntSeparationRightDistance ); + + // Dont allow crossing through the spawn room visualizers + ret = !PointsCrossRespawnRoomVisualizer( vecStartCenter, vecEndSuperSafe ); + } + + + return ret; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::IsAllowedToInitiateTauntWithPartner( CEconItemView *pEconItemView, char *pszErrorMessage, int cubErrorMessage ) +{ + Vector vecEnd; + float flTolerance; + + if ( TFGameRules() && TFGameRules()->ShowMatchSummary() ) + return true; + + bool ret = FindOpenTauntPartnerPosition( pEconItemView, vecEnd, &flTolerance ); + + // Check that there isn't too much space underneath the destination. + if ( ret ) + { + trace_t result3; + CTraceFilterIgnoreTeammates filter( this, COLLISION_GROUP_NONE, GetTeamNumber() ); + UTIL_TraceHull( vecEnd, vecEnd - Vector( 0, 0, flTolerance ), VEC_HULL_MIN, VEC_HULL_MAX, MASK_PLAYERSOLID, &filter, &result3 ); + if ( !result3.DidHit() ) + { + if ( pszErrorMessage && cubErrorMessage > 0 ) + { + V_strncpy( pszErrorMessage, "#TF_PartnerTaunt_TooHigh", cubErrorMessage ); + } + + ret = false; + } + } + else if ( pszErrorMessage && cubErrorMessage > 0 ) + { + V_strncpy( pszErrorMessage, "#TF_PartnerTaunt_Blocked", cubErrorMessage ); + } + + return ret; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::IsWormsGearEquipped( void ) const +{ + // If we have the Worms Gear equipped, play their custom sound + static CSchemaItemDefHandle ppItemDefWearables[] = { CSchemaItemDefHandle( "Worms Gear" ) }; + return HasWearablesEquipped( ppItemDefWearables, ARRAYSIZE( ppItemDefWearables ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::IsRobotCostumeEquipped( void ) const +{ + if ( GetPlayerClass()->GetClassIndex() != TF_CLASS_SOLDIER ) + return false; + + static CSchemaItemDefHandle ppItemDefWearables[] = { CSchemaItemDefHandle( "Idiot Box" ), CSchemaItemDefHandle( "Steel Pipes" ), CSchemaItemDefHandle( "Shoestring Budget" ) }; + return HasWearablesEquipped( ppItemDefWearables, ARRAYSIZE( ppItemDefWearables ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::IsDemowolf( void ) const +{ + if ( GetPlayerClass()->GetClassIndex() != TF_CLASS_DEMOMAN ) + return false; + + static CSchemaItemDefHandle ppItemDefWearables[] = { CSchemaItemDefHandle( "Hair of the Dog" ), CSchemaItemDefHandle( "Scottish Snarl" ), CSchemaItemDefHandle( "Pickled Paws" ) }; + return HasWearablesEquipped( ppItemDefWearables, ARRAYSIZE( ppItemDefWearables ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::IsFrankenHeavy( void ) const +{ + if ( GetPlayerClass()->GetClassIndex() != TF_CLASS_HEAVYWEAPONS ) + return false; + + static CSchemaItemDefHandle ppItemDefWearables[] = { CSchemaItemDefHandle( "Can Opener" ), CSchemaItemDefHandle( "Soviet Stitch-Up" ), CSchemaItemDefHandle( "Steel-Toed Stompers" ) }; + return HasWearablesEquipped( ppItemDefWearables, ARRAYSIZE( ppItemDefWearables ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::IsFairyHeavy( void ) const +{ + if ( GetPlayerClass()->GetClassIndex() != TF_CLASS_HEAVYWEAPONS ) + return false; + + static CSchemaItemDefHandle ppItemDefWearables[] = { CSchemaItemDefHandle( "The Grand Duchess Tutu" ), CSchemaItemDefHandle( "The Grand Duchess Fairy Wings" ), CSchemaItemDefHandle( "The Grand Duchess Tiara" ) }; + return HasWearablesEquipped( ppItemDefWearables, ARRAYSIZE( ppItemDefWearables ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::IsZombieCostumeEquipped( void ) const +{ + int iZombie = 0; + CALL_ATTRIB_HOOK_INT( iZombie, zombiezombiezombiezombie ); + return iZombie != 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::HasWearablesEquipped( const CSchemaItemDefHandle *ppItemDefs, int nWearables ) const +{ + for ( int i = 0; i < nWearables; i++ ) + { + const CEconItemDefinition *pItemDef = ppItemDefs[i]; + + // Backwards because our wearable items are probably sitting in our cosmetic slots near + // the end of our list. + bool bHasWearable = false; + + FOR_EACH_VEC_BACK( m_hMyWearables, wbl ) + { + CEconWearable *pWearable = m_hMyWearables[wbl]; + if ( pWearable && + pWearable->GetAttributeContainer()->GetItem() && + pWearable->GetAttributeContainer()->GetItem()->GetItemDefinition() == pItemDef ) + { + bHasWearable = true; + break; + } + } + + if ( !bHasWearable ) + { + return false; + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the current concept for press-and-hold taunts or MP_CONCEPT_NONE if none is available. +//----------------------------------------------------------------------------- +int CTFPlayer::GetTauntConcept( CEconItemDefinition *pItemDef ) +{ + for ( int i=0; i<pItemDef->GetNumAnimations( GetTeamNumber() ); ++i ) + { + animation_on_wearable_t* pAnim = pItemDef->GetAnimationData( GetTeamNumber(), i ); + if ( pAnim && pAnim->pszActivity && + !Q_stricmp( pAnim->pszActivity, "taunt_concept" ) ) + { + const char* pszConcept = pAnim->pszReplacement; + if ( !pszConcept ) + return true; + + return GetMPConceptIndexFromString( pszConcept ); + } + } + + return MP_CONCEPT_NONE; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::PlayTauntSceneFromItem( CEconItemView *pEconItemView ) +{ + if ( !pEconItemView ) + return false; + + if ( !IsAllowedToTaunt() ) + return false; + + const GameItemDefinition_t *pItemDef = pEconItemView->GetItemDefinition(); + if ( !pItemDef ) + return false; + + CTFTauntInfo *pTauntData = pItemDef->GetTauntData(); + if ( !pTauntData ) + return false; + + int iClass = GetPlayerClass()->GetClassIndex(); + + // If we didn't find any custom taunts, then we're done + if ( pTauntData->GetIntroSceneCount( iClass ) == 0 ) + { + return false; + } + + int iScene = RandomInt( 0, pTauntData->GetIntroSceneCount( iClass ) - 1 ); + const char* pszScene = pTauntData->GetIntroScene( iClass, iScene ); + if ( pszScene ) + { + int iTauntIndex = TAUNT_MISC_ITEM; + int iTauntConcept = 0; + + // check if this is a long taunt + static CSchemaAttributeDefHandle pAttrDef_TauntPressAndHold( "taunt is press and hold" ); + attrib_value_t iLongTaunt = 0; + if ( pEconItemView->FindAttribute( pAttrDef_TauntPressAndHold, &iLongTaunt ) && iLongTaunt != 0 ) + { + iTauntIndex = TAUNT_LONG; + iTauntConcept = pTauntData->IsPartnerTaunt() ? MP_CONCEPT_PARTNER_TAUNT_READY : iTauntConcept; + m_bIsTauntInitiator = true; + + ParseSharedTauntDataFromEconItemView( pEconItemView ); + + /*cant we just network over the "taunting item id", since client and server both know all the item defs, + then they can both look at attributes and we dont need to keep networking more and more stuff?*/ + // check if this taunt can be mimic by other players + static CSchemaAttributeDefHandle pAttrDef_TauntMimic( "taunt mimic" ); + attrib_value_t iTauntMimic = 0; + pEconItemView->FindAttribute( pAttrDef_TauntMimic, &iTauntMimic ); + m_bTauntMimic = iTauntMimic != 0; + + // check if we can initiate partner taunt (ignore mimic taunt to allow Conga initiation) + char szClientError[64]; + if ( !m_bTauntMimic && pTauntData->IsPartnerTaunt() && !IsAllowedToInitiateTauntWithPartner( pEconItemView, szClientError, ARRAYSIZE( szClientError ) ) ) + { + CSingleUserRecipientFilter filter( this ); + EmitSound_t params; + params.m_pSoundName = "Player.DenyWeaponSelection"; + EmitSound( filter, entindex(), params ); + + TFGameRules()->SendHudNotification( filter, szClientError, "ico_notify_partner_taunt" ); + + return false; + } + } + + // Store this off so eventually we can let clients know which item ID is doing this taunt. + m_iTauntItemDefIndex = pEconItemView->GetItemDefIndex(); + m_TauntEconItemView = *pEconItemView; + + // Should we play a sound? + m_strTauntSoundName = ""; + m_flTauntSoundTime = 0.f; + static CSchemaAttributeDefHandle pAttrDef_TauntSuccessSound( "taunt success sound" ); + CAttribute_String attrTauntSuccessSound; + if ( pEconItemView->FindAttribute( pAttrDef_TauntSuccessSound, &attrTauntSuccessSound ) ) + { + const char* pszTauntSoundName = attrTauntSuccessSound.value().c_str(); + Assert( pszTauntSoundName && *pszTauntSoundName ); + if ( pszTauntSoundName && *pszTauntSoundName ) + { + m_strTauntSoundName = pszTauntSoundName; + + static CSchemaAttributeDefHandle pAttrDef_TauntSuccessSoundOffset( "taunt success sound offset" ); + attrib_value_t attrTauntSoundOffset = 0; + pEconItemView->FindAttribute( pAttrDef_TauntSuccessSoundOffset, &attrTauntSoundOffset ); + float flTauntSoundOffset = (float&)attrTauntSoundOffset; + m_flTauntSoundTime = gpGlobals->curtime + flTauntSoundOffset; + } + } + + // Should we play a looping sound? + m_flTauntSoundLoopTime = 0.f; + Assert( m_strTauntSoundLoopName.IsEmpty() ); + m_strTauntSoundLoopName = ""; + static CSchemaAttributeDefHandle pAttrDef_TauntSuccessSoundLoop( "taunt success sound loop" ); + CAttribute_String attrTauntSuccessSoundLoop; + if ( pEconItemView->FindAttribute( pAttrDef_TauntSuccessSoundLoop, &attrTauntSuccessSoundLoop ) ) + { + const char* pszTauntSoundLoopName = attrTauntSuccessSoundLoop.value().c_str(); + Assert( pszTauntSoundLoopName && *pszTauntSoundLoopName ); + if ( pszTauntSoundLoopName && *pszTauntSoundLoopName ) + { + // play the looping sounds using the envelope controller + m_strTauntSoundLoopName = pszTauntSoundLoopName; + + static CSchemaAttributeDefHandle pAttrDef_TauntSuccessSoundLoopOffset( "taunt success sound loop offset" ); + attrib_value_t attrTauntSoundLoopOffset = 0; + pEconItemView->FindAttribute( pAttrDef_TauntSuccessSoundLoopOffset, &attrTauntSoundLoopOffset ); + float flTauntSoundLoopOffset = (float&)attrTauntSoundLoopOffset; + m_flTauntSoundLoopTime = gpGlobals->curtime + flTauntSoundLoopOffset; + } + } + + m_iTauntAttack = TAUNTATK_NONE; + m_flTauntAttackTime = 0.f; + + static CSchemaAttributeDefHandle pAttrDef_TauntAttackName( "taunt attack name" ); + const char* pszTauntAttackName = NULL; + if ( FindAttribute_UnsafeBitwiseCast<CAttribute_String>( pItemDef, pAttrDef_TauntAttackName, &pszTauntAttackName ) ) + { + m_iTauntAttack = GetTauntAttackByName( pszTauntAttackName ); + } + + static CSchemaAttributeDefHandle pAttrDef_TauntAttackTime( "taunt attack time" ); + float flTauntAttackTime = 0.f; + if ( FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pItemDef, pAttrDef_TauntAttackTime, &flTauntAttackTime ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + flTauntAttackTime; + } + + m_iPreTauntWeaponSlot = -1; + if ( GetActiveWeapon() ) + { + m_iPreTauntWeaponSlot = GetActiveWeapon()->GetSlot(); + } + static CSchemaAttributeDefHandle pAttrDef_TauntForceWeaponSlot( "taunt force weapon slot" ); + const char* pszTauntForceWeaponSlotName = NULL; + if ( FindAttribute_UnsafeBitwiseCast<CAttribute_String>( pItemDef, pAttrDef_TauntForceWeaponSlot, &pszTauntForceWeaponSlotName ) ) + { + int iForceWeaponSlot = StringFieldToInt( pszTauntForceWeaponSlotName, GetItemSchema()->GetWeaponTypeSubstrings() ); + Weapon_Switch( Weapon_GetSlot( iForceWeaponSlot ) ); + } + + m_bInitTaunt = true; + + // Allow voice commands, etc to be interrupted. + CMultiplayer_Expresser *pExpresser = GetMultiplayerExpresser(); + Assert( pExpresser ); + pExpresser->AllowMultipleScenes(); + + float flSceneDuration = PlayScene( pszScene ); + OnTauntSucceeded( pszScene, iTauntIndex, iTauntConcept ); + + m_flNextAllowTauntRemapInputTime = iTauntIndex == TAUNT_LONG ? gpGlobals->curtime + flSceneDuration : -1.f; + + pExpresser->DisallowMultipleScenes(); + + const char *pszTauntProp = pTauntData->GetProp( iClass ); + if ( pszTauntProp ) + { + const char *pszTauntPropScene = pTauntData->GetPropIntroScene( iClass ); + if ( pszTauntPropScene ) + { + CTFTauntProp *pProp = static_cast< CTFTauntProp * >( CreateEntityByName( "tf_taunt_prop" ) ); + if ( pProp ) + { + pProp->SetModel( pszTauntProp ); + + pProp->m_nSkin = GetTeamNumber() == TF_TEAM_RED ? 0 : 1; + DispatchSpawn( pProp ); + pProp->SetAbsOrigin( GetAbsOrigin() ); + pProp->SetAbsAngles( GetAbsAngles() ); + pProp->SetEFlags( EFL_FORCE_CHECK_TRANSMIT ); + + pProp->PlayScene( pszTauntPropScene ); + + m_hTauntProp = pProp; + } + } + else + { + CTFWeaponBase *pWeapon = GetActiveTFWeapon(); + if ( pWeapon && pWeapon->HideAttachmentsAndShowBodygroupsWhenPerformingWeaponIndependentTaunt() ) + { + // If there's no prop scene, our weapon is being repurposed + pWeapon->SetIsBeingRepurposedForTaunt( true ); + } + } + } + + // check for achievement + static CSchemaItemDefHandle congaTaunt( "Conga Taunt" ); + if ( pEconItemView->GetItemDefinition() == congaTaunt ) + { + CUtlVector< CTFPlayer * > vecPlayers; + CollectPlayers( &vecPlayers, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &vecPlayers, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + CUtlVector< CTFPlayer * > vecCongaLine; + + FOR_EACH_VEC( vecPlayers, i ) + { + CTFPlayer *pPlayer = vecPlayers[i]; + if ( pPlayer && pPlayer->m_Shared.InCond( TF_COND_TAUNTING ) ) + { + // is this player doing the Conga? + if ( pPlayer->GetTauntEconItemView() && ( pPlayer->GetTauntEconItemView()->GetItemDefinition() == congaTaunt ) ) + { + vecCongaLine.AddToTail( pPlayer ); + } + } + } + + if ( vecCongaLine.Count() >= 10 ) + { + FOR_EACH_VEC( vecCongaLine, i ) + { + CTFPlayer *pPlayer = vecCongaLine[i]; + if ( pPlayer ) + { + pPlayer->AwardAchievement( ACHIEVEMENT_TF_TAUNT_CONGA_LINE ); + } + } + } + } + + // override FOV + m_iPreTauntFOV = GetFOV(); + if ( pTauntData->GetFOV() != 0 ) + { + SetFOV( this, pTauntData->GetFOV() ); + } + +#ifdef STAGING_ONLY + if ( tf_tauntcam_fov_override.GetInt() != 0 ) + { + SetFOV( this, tf_tauntcam_fov_override.GetInt() ); + } +#endif // STAGING_ONLY + + m_TauntStage = TAUNT_INTRO; + + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTFPlayer::PlayTauntRemapInputScene() +{ + CTFTauntInfo *pTaunt = m_TauntEconItemView.GetStaticData()->GetTauntData(); + if ( !pTaunt ) + { + return -1.f; + } + + if ( m_TauntStage != TAUNT_INTRO ) + { + return -1.f; + } + + int iClass = GetPlayerClass()->GetClassIndex(); + + const char *pszCurrentSceneFileName = GetSceneFilename( m_hTauntScene ); + + const char *pszSceneName = NULL; + for ( int iButtonIndex=0; iButtonIndex<pTaunt->GetTauntInputRemapCount(); ++iButtonIndex ) + { + const CTFTauntInfo::TauntInputRemap_t& tauntRemap = pTaunt->GetTauntInputRemapScene( iButtonIndex ); + if ( tauntRemap.m_vecButtonPressedScenes[iClass].IsEmpty() ) + continue; + + if ( m_afButtonPressed & tauntRemap.m_iButton ) + { + int iRandomTaunt = RandomInt( 0, tauntRemap.m_vecButtonPressedScenes[iClass].Count() - 1 ); + pszSceneName = tauntRemap.m_vecButtonPressedScenes[iClass][iRandomTaunt]; + break; + } + + const char *pszPressedScene = tauntRemap.m_vecButtonPressedScenes[iClass][0]; + if ( m_nButtons & tauntRemap.m_iButton ) + { + // already in this scene, try again later for next state + if ( FStrEq( pszCurrentSceneFileName, pszPressedScene ) ) + { + return 0.f; + } + + pszSceneName = pszPressedScene; + break; + } + else if ( FStrEq( pszCurrentSceneFileName, pszPressedScene ) && !tauntRemap.m_vecButtonReleasedScenes[iClass].IsEmpty() ) + { + int iRandomTaunt = RandomInt( 0, tauntRemap.m_vecButtonReleasedScenes[iClass].Count() - 1 ); + pszSceneName = tauntRemap.m_vecButtonReleasedScenes[iClass][iRandomTaunt]; + break; + } + } + + if ( pszSceneName ) + { + StopScriptedScene( this, m_hTauntScene ); + m_hTauntScene = NULL; + + CMultiplayer_Expresser *pInitiatorExpresser = GetMultiplayerExpresser(); + Assert( pInitiatorExpresser ); + + pInitiatorExpresser->AllowMultipleScenes(); + + // extend initiator's taunt duration to include actual high five + m_bInitTaunt = true; + + float flSceneDuration = PlayScene( pszSceneName ); + + m_bInitTaunt = false; + pInitiatorExpresser->DisallowMultipleScenes(); + + return flSceneDuration; + } + + return 0.f; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::OnTauntSucceeded( const char* pszSceneName, int iTauntIndex /*= 0*/, int iTauntConcept /*= 0*/ ) +{ + float flDuration = GetSceneDuration( pszSceneName ) + 0.2f; + + float flDurationMod = 1; + CALL_ATTRIB_HOOK_FLOAT( flDurationMod, mult_gesture_time ); // Modify by attributes. + flDuration /= flDurationMod; + + // Set player state as taunting. + m_Shared.m_iTauntIndex = iTauntIndex; + m_Shared.m_iTauntConcept.Set( iTauntConcept ); + m_flTauntStartTime = gpGlobals->curtime; + + const itemid_t unTauntSourceItemID = m_TauntEconItemView.IsValid() ? m_TauntEconItemView.GetItemID() : INVALID_ITEM_ID; + m_Shared.m_unTauntSourceItemID_Low = unTauntSourceItemID & 0xffffffff; + m_Shared.m_unTauntSourceItemID_High = (unTauntSourceItemID >> 32) & 0xffffffff; + m_Shared.AddCond( TF_COND_TAUNTING ); + + if ( iTauntIndex == TAUNT_LONG ) + { + m_flTauntRemoveTime = gpGlobals->curtime; + m_bAllowedToRemoveTaunt = false; + if ( iTauntConcept == MP_CONCEPT_PARTNER_TAUNT_READY ) + { + GetReadyToTauntWithPartner(); + } + + m_flTauntYaw = BodyAngles().y; + } + else + { + m_flTauntRemoveTime = gpGlobals->curtime + flDuration; + m_bAllowedToRemoveTaunt = true; + } + + m_angTauntCamera = EyeAngles(); + + // Slam velocity to zero. + SetAbsVelocity( vec3_origin ); + + // play custom set taunt particle if we have a full set equipped + if ( IsPlayerClass( TF_CLASS_SPY ) ) + { + // FIX ME: We should be using string attribute type instead of float when we add code support to it + // Hand Coded for this effect which may change later + int iCustomTauntParticle = 0; + CALL_ATTRIB_HOOK_INT( iCustomTauntParticle, custom_taunt_particle_attr ); + if ( iCustomTauntParticle ) + { + DispatchParticleEffect( "set_taunt_saharan_spy", PATTACH_ABSORIGIN_FOLLOW, this ); + } + } + + // set initial taunt yaw to make sure that the client anim not off because of lag + SetTauntYaw( GetAbsAngles()[YAW] ); + + m_vecTauntStartPosition = GetAbsOrigin(); + + // Strange Taunts + EconItemInterface_OnOwnerKillEaterEventNoPartner( &m_TauntEconItemView, this, kKillEaterEvent_TauntsPerformed ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::Taunt( taunts_t iTauntIndex, int iTauntConcept ) +{ + if ( !IsAllowedToTaunt() ) + return; + + if ( iTauntIndex == TAUNT_LONG ) + { + AssertMsg( false, "Long Taunt should be using the new system which reads scene names from item definitions" ); + return; + } + + // Heavies can purchase a rage-based knockback+stun effect in MvM, + // so ignore taunt and activate rage if we're at full rage + if ( IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ) + { + if ( GetActiveTFWeapon() && GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_MINIGUN ) + { + int iRage = 0; + CALL_ATTRIB_HOOK_INT( iRage, generate_rage_on_dmg ); + if ( iRage ) + { + if ( m_Shared.GetRageMeter() >= 100.f ) + { + m_Shared.m_bRageDraining = true; + EmitSound( "Heavy.Battlecry03" ); + return; + } + + if ( m_Shared.IsRageDraining() ) + return; + } + } + } + + // Allow voice commands, etc to be interrupted. + CMultiplayer_Expresser *pExpresser = GetMultiplayerExpresser(); + Assert( pExpresser ); + pExpresser->AllowMultipleScenes(); + + m_hTauntItem = NULL; + + m_bInitTaunt = true; + char szResponse[AI_Response::MAX_RESPONSE_NAME]; + bool bTauntSucceeded = false; + switch ( iTauntIndex ) + { + case TAUNT_SHOW_ITEM: + iTauntConcept = MP_CONCEPT_PLAYER_SHOW_ITEM_TAUNT; + break; + + // use the concept specified for these two + case TAUNT_MISC_ITEM: + case TAUNT_SPECIAL: + break; + + default: + case TAUNT_BASE_WEAPON: + iTauntConcept = MP_CONCEPT_PLAYER_TAUNT; + break; + }; + + bTauntSucceeded = SpeakConceptIfAllowed( iTauntConcept, NULL, szResponse, AI_Response::MAX_RESPONSE_NAME ); + if ( bTauntSucceeded ) + { + OnTauntSucceeded( szResponse, iTauntIndex, iTauntConcept ); + } + else + { + m_bInitTaunt = false; + } + + pExpresser->DisallowMultipleScenes(); + + m_flTauntAttackTime = 0; + m_iTauntAttack = TAUNTATK_NONE; + + if ( !bTauntSucceeded ) + return; + + // should we play a sound? + CAttribute_String attrCosmeticTauntSound; + CALL_ATTRIB_HOOK_STRING( attrCosmeticTauntSound, cosmetic_taunt_sound ); + const char* pszTauntSoundName = attrCosmeticTauntSound.value().c_str(); + if ( pszTauntSoundName && *pszTauntSoundName ) + { + EmitSound( pszTauntSoundName ); + } + + if ( iTauntIndex == TAUNT_SHOW_ITEM ) + { + m_flTauntAttackTime = gpGlobals->curtime + 1.5; + m_iTauntAttack = TAUNTATK_SHOW_ITEM; + return; + } + + CTFWeaponBase *pActiveWeapon = m_Shared.GetActiveTFWeapon(); + if ( iTauntIndex == TAUNT_BASE_WEAPON ) + { + // phlogistinator + if ( IsPlayerClass( TF_CLASS_PYRO ) && m_Shared.GetRageMeter() >= 100.0f && + StringHasPrefix( szResponse, "scenes/player/pyro/low/taunt01" ) ) + { + // Pyro Rage! + CBaseCombatWeapon *pWeapon = GetActiveWeapon(); + if ( pWeapon ) + { + int iBuffType = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iBuffType, set_buff_type ); + + if ( iBuffType > 0 ) + { + // Time for crits! + m_Shared.ActivateRageBuff( this, iBuffType ); + + // Pyro needs high defense while he's taunting + //m_Shared.AddCond( TF_COND_DEFENSEBUFF_HIGH, 3.0f ); + m_Shared.AddCond( TF_COND_INVULNERABLE_USER_BUFF, 2.60f ); + m_Shared.AddCond( TF_COND_MEGAHEAL, 2.60f ); + } + } + } + else if ( IsPlayerClass( TF_CLASS_SCOUT ) ) + { + if ( m_Shared.InCond( TF_COND_PHASE ) == false ) + { + if ( pActiveWeapon && pActiveWeapon->GetWeaponID() == TF_WEAPON_LUNCHBOX ) + { + m_flTauntAttackTime = gpGlobals->curtime + 0.9; + m_iTauntAttack = TAUNTATK_SCOUT_DRINK; + } + } + } + else if ( IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ) + { + if ( pActiveWeapon && pActiveWeapon->GetWeaponID() == TF_WEAPON_LUNCHBOX ) + { + m_flTauntAttackTime = gpGlobals->curtime + 1.0; + m_iTauntAttack = TAUNTATK_HEAVY_EAT; + + // Only count sandviches for "eat 100 sandviches" achievement + CTFLunchBox *pLunchbox = (CTFLunchBox*)pActiveWeapon; + if ( ( pLunchbox->GetLunchboxType() == LUNCHBOX_STANDARD ) || ( pLunchbox->GetLunchboxType() == LUNCHBOX_STANDARD_ROBO ) ) + { + AwardAchievement( ACHIEVEMENT_TF_HEAVY_EAT_SANDWICHES ); + } + } + } + } + else if ( iTauntIndex == TAUNT_SPECIAL ) + { + if ( IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + // Wrenchmotron taunt teleport home effect + if ( !Q_stricmp( szResponse, "scenes/player/engineer/low/taunt_drg_melee.vcd" ) ) + { + m_bIsTeleportingUsingEurekaEffect = true; + + m_teleportHomeFlashTimer.Start( 1.9f ); + + // play teleport sound at location we are leaving + Vector soundOrigin = WorldSpaceCenter(); + CPASAttenuationFilter filter( soundOrigin ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + ep.m_pSoundName = "Weapon_DRG_Wrench.Teleport"; + ep.m_flVolume = 1.0f; + ep.m_SoundLevel = SNDLVL_150dB; + ep.m_nFlags = 0; + ep.m_nPitch = PITCH_NORM; + ep.m_pOrigin = &soundOrigin; + + int worldEntIndex = 0; + EmitSound( filter, worldEntIndex, ep ); + } + } + } + + // Setup taunt attacks. Hacky, but a lot easier to do than getting server side anim events working. + if ( IsPlayerClass(TF_CLASS_PYRO) ) + { + if ( !V_stricmp( szResponse, "scenes/player/pyro/low/taunt02.vcd" ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + 2.1f; + m_iTauntAttack = TAUNTATK_PYRO_HADOUKEN; + } + else if ( !V_stricmp( szResponse, "scenes/player/pyro/low/taunt_bubbles.vcd" ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + 3.0f; + m_iTauntAttack = TAUNTATK_PYRO_ARMAGEDDON; + + // We need to parent this to a target instead of the player because the player changing their camera view can twist the rainbow + CBaseEntity *pTarget = CreateEntityByName( "info_target" ); + if ( pTarget ) + { + DispatchSpawn( pTarget ); + pTarget->SetAbsOrigin( GetAbsOrigin() ); + pTarget->SetAbsAngles( GetAbsAngles() ); + pTarget->SetEFlags( EFL_FORCE_CHECK_TRANSMIT ); + pTarget->SetThink( &BaseClass::SUB_Remove ); + pTarget->SetNextThink( gpGlobals->curtime + 8.0f ); + + CBaseEntity *pGround = GetGroundEntity(); + if ( pGround && pGround->GetMoveType() == MOVETYPE_PUSH ) + { + pTarget->SetParent( pGround ); + } + } + + DispatchParticleEffect( "pyrotaunt_rainbow_norainbow", PATTACH_ABSORIGIN_FOLLOW, pTarget ); + } + else if ( !V_stricmp( szResponse, "scenes/player/pyro/low/taunt_scorch_shot.vcd" ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + 1.9f; + m_iTauntAttack = TAUNTATK_PYRO_SCORCHSHOT; + } + } + else if ( IsPlayerClass(TF_CLASS_HEAVYWEAPONS) ) + { + if ( !V_stricmp( szResponse, "scenes/player/heavy/low/taunt03_v1.vcd" ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + 1.8; + m_iTauntAttack = TAUNTATK_HEAVY_HIGH_NOON; + } + else if ( pActiveWeapon && pActiveWeapon->GetWeaponID() == TF_WEAPON_FISTS ) + { + CTFFists *pFists = dynamic_cast<CTFFists*>(pActiveWeapon); + if ( pFists && pFists->GetFistType() == FISTTYPE_RADIAL_BUFF ) + { + m_flTauntAttackTime = gpGlobals->curtime + 1.0; + m_iTauntAttack = TAUNTATK_HEAVY_RADIAL_BUFF; + } + } + } + else if ( IsPlayerClass( TF_CLASS_SCOUT ) ) + { + if ( !V_stricmp( szResponse, "scenes/player/scout/low/taunt05_v1.vcd" ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + 4.03f; + m_iTauntAttack = TAUNTATK_SCOUT_GRAND_SLAM; + } + } + else if ( IsPlayerClass( TF_CLASS_MEDIC ) ) + { + if ( !V_stricmp( szResponse, "scenes/player/medic/low/taunt06.vcd" ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + 0.8f; + m_flTauntInhaleTime = gpGlobals->curtime + 1.8f; + + const char *pszParticleEffect; + pszParticleEffect = ( GetTeamNumber() == TF_TEAM_RED ? "healhuff_red" : "healhuff_blu" ); + DispatchParticleEffect( pszParticleEffect, PATTACH_POINT_FOLLOW, this, "eyes" ); + + m_iTauntAttack = TAUNTATK_MEDIC_INHALE; + } + else if ( !V_stricmp( szResponse, "scenes/player/medic/low/taunt08.vcd" ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + 2.2f; + m_iTauntAttack = TAUNTATK_MEDIC_UBERSLICE_IMPALE; + } + } + else if ( IsPlayerClass( TF_CLASS_SPY ) ) + { + if ( !V_strnicmp( szResponse, "scenes/player/spy/low/taunt03", 29 ) ) // There's taunt03_v1 & taunt03_v2 + { + m_flTauntAttackTime = gpGlobals->curtime + 1.8f; + m_iTauntAttack = TAUNTATK_SPY_FENCING_SLASH_A; + } + } + else if ( IsPlayerClass( TF_CLASS_SNIPER ) ) + { + if ( !V_stricmp( szResponse, "scenes/player/sniper/low/taunt04.vcd" ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + 0.85f; + m_iTauntAttack = TAUNTATK_SNIPER_ARROW_STAB_IMPALE; + } + } + else if ( IsPlayerClass( TF_CLASS_SOLDIER ) ) + { + if ( !V_stricmp( szResponse, "scenes/player/soldier/low/taunt05.vcd" ) ) + { + if ( IsWormsGearEquipped() ) + { + m_flTauntAttackTime = gpGlobals->curtime + 1.4f; + m_iTauntAttack = TAUNTATK_SOLDIER_GRENADE_KILL_WORMSIGN; + return; + } + + m_flTauntAttackTime = gpGlobals->curtime + 3.5f; + m_iTauntAttack = TAUNTATK_SOLDIER_GRENADE_KILL; + } + } + else if ( IsPlayerClass( TF_CLASS_DEMOMAN ) ) + { + if ( !V_stricmp( szResponse, "scenes/player/demoman/low/taunt09.vcd" ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + 2.55f; + m_iTauntAttack = TAUNTATK_DEMOMAN_BARBARIAN_SWING; + } + } + else if ( IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + if ( !V_stricmp( szResponse, "scenes/player/engineer/low/taunt07.vcd" ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + 3.695f; + m_iTauntAttack = TAUNTATK_ENGINEER_GUITAR_SMASH; + } + else if ( !V_stricmp( szResponse, "scenes/player/engineer/low/taunt09.vcd" ) ) + { + m_flTauntAttackTime = gpGlobals->curtime + 3.2f; + m_iTauntAttack = TAUNTATK_ENGINEER_ARM_IMPALE; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Aborts a taunt in progress. +//----------------------------------------------------------------------------- +void CTFPlayer::CancelTaunt( void ) +{ + m_bIsTeleportingUsingEurekaEffect = false; + m_teleportHomeFlashTimer.Reset(); + + StopTaunt(); +} + +//----------------------------------------------------------------------------- +// Purpose: Stops taunting +//----------------------------------------------------------------------------- +void CTFPlayer::StopTaunt( void ) +{ + if ( m_hTauntScene.Get() ) + { + StopScriptedScene( this, m_hTauntScene ); + m_flTauntRemoveTime = 0.0f; + m_bAllowedToRemoveTaunt = true; + m_hTauntScene = NULL; + } + + if ( m_hTauntProp.Get() ) + { + UTIL_Remove( m_hTauntProp ); + m_hTauntProp = NULL; + } + + if ( IsReadyToTauntWithPartner() ) + { + CancelTauntWithPartner(); + } + + StopTauntSoundLoop(); + + // reset the FOV + if ( m_TauntEconItemView.IsValid() ) + { + SetFOV( this, m_iPreTauntFOV ); + } + + m_hHighFivePartner = NULL; + m_bAllowMoveDuringTaunt = false; + m_flTauntOutroTime = 0.f; + m_bTauntForceMoveForward = false; + m_flTauntForceMoveForwardSpeed = 0.f; + m_flTauntMoveAccelerationTime = 0.f; + m_flTauntTurnSpeed = 0.f; + m_flTauntTurnAccelerationTime = 0.f; + m_bTauntMimic = false; + m_bIsTauntInitiator = false; + m_TauntEconItemView.Invalidate(); + m_flNextAllowTauntRemapInputTime = -1.f; + m_flCurrentTauntMoveSpeed = 0.f; + m_nActiveTauntSlot = LOADOUT_POSITION_INVALID; + m_iTauntItemDefIndex = INVALID_ITEM_DEF_INDEX; + m_TauntStage = TAUNT_NONE; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::EndLongTaunt() +{ + Assert( m_Shared.GetTauntIndex() == TAUNT_LONG ); + + m_bAllowedToRemoveTaunt = true; + m_flTauntRemoveTime = gpGlobals->curtime; + + int iClass = GetPlayerClass()->GetClassIndex(); + CTFTauntInfo *pTauntData = m_TauntEconItemView.GetStaticData()->GetTauntData(); + if ( pTauntData ) + { + // Make sure press-and-hold taunts last a minimum amount of time + float flMinTime = pTauntData->GetMinTauntTime(); + if ( m_flTauntStartTime + flMinTime > gpGlobals->curtime ) + { + m_flTauntRemoveTime = m_flTauntStartTime + flMinTime; + } + + // should we play outro? + if ( pTauntData->GetOutroSceneCount( iClass ) > 0 ) + { + m_bAllowedToRemoveTaunt = false; + m_flTauntOutroTime = m_flTauntRemoveTime; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTFPlayer::PlayTauntOutroScene() +{ + m_TauntStage = TAUNT_OUTRO; + + float flDuration = 0.f; + int iClass = GetPlayerClass()->GetClassIndex(); + CTFTauntInfo *pTauntData = m_TauntEconItemView.GetStaticData()->GetTauntData(); + if ( pTauntData ) + { + if ( pTauntData->GetOutroSceneCount( iClass ) > 0 ) + { + // play outro + const char *pszOutroScene = pTauntData->GetOutroScene( iClass, RandomInt( 0, pTauntData->GetOutroSceneCount( iClass ) - 1 ) ); + if ( m_hTauntScene.Get() ) + { + StopScriptedScene( this, m_hTauntScene ); + m_hTauntScene = NULL; + + StopTauntSoundLoop(); + } + + // Allow voice commands, etc to be interrupted. + CMultiplayer_Expresser *pExpresser = GetMultiplayerExpresser(); + Assert( pExpresser ); + pExpresser->AllowMultipleScenes(); + + m_bInitTaunt = true; + + flDuration = PlayScene( pszOutroScene ); + OnTauntSucceeded( pszOutroScene, TAUNT_MISC_ITEM, MP_CONCEPT_HIGHFIVE_SUCCESS ); + + m_bInitTaunt = false; + + pExpresser->DisallowMultipleScenes(); + + if ( m_hTauntProp != NULL ) + { + const char *pszPropScene = pTauntData->GetPropOutroScene( iClass ); + if ( pszPropScene ) + { + m_hTauntProp->PlayScene( pszPropScene ); + } + } + } + } + + return flDuration; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::HandleTauntCommand( int iTauntSlot ) +{ + if ( !IsAllowedToTaunt() ) + return; + + m_nActiveTauntSlot = LOADOUT_POSITION_INVALID; + if ( iTauntSlot > 0 && iTauntSlot <= 8 ) + { + m_nActiveTauntSlot = LOADOUT_POSITION_TAUNT + iTauntSlot - 1; + CEconItemView* pItem = GetEquippedItemForLoadoutSlot( m_nActiveTauntSlot ); + PlayTauntSceneFromItem( pItem ); + return; + } + else + { + // Check if I should accept taunt with partner + CTFPlayer *initiator = FindPartnerTauntInitiator(); + if ( initiator ) + { + if ( initiator->m_bTauntMimic ) + { + MimicTauntFromPartner( initiator ); + } + else + { + AcceptTauntWithPartner( initiator ); + } + return; + } + + // does this weapon prevent player from doing manual taunt? + CTFWeaponBase *pActiveWeapon = m_Shared.GetActiveTFWeapon(); + if ( pActiveWeapon && !pActiveWeapon->AllowTaunts() ) + return; + + Taunt( TAUNT_BASE_WEAPON ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ClearTauntAttack() +{ + m_flTauntAttackTime = 0.f; + m_flTauntInhaleTime = 0.f; + m_iTauntAttack = TAUNTATK_NONE; + m_iTauntAttackCount = 0; + m_iTauntRPSResult = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::HandleWeaponSlotAfterTaunt() +{ + if ( m_iPreTauntWeaponSlot != -1 ) + { + // switch back to the active weapon before taunting + Weapon_Switch( Weapon_GetSlot( m_iPreTauntWeaponSlot ) ); + m_iPreTauntWeaponSlot = -1; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static void DispatchRPSEffect( const CTFPlayer *pPlayer, const char* pszParticleName ) +{ + CEffectData data; + data.m_nHitBox = GetParticleSystemIndex( pszParticleName ); + data.m_vOrigin = pPlayer->GetAbsOrigin() + Vector( 0, 0, 87.0f ); + data.m_vAngles = vec3_angle; + + CPASFilter intiatorFilter( data.m_vOrigin ); + intiatorFilter.SetIgnorePredictionCull( true ); + + te->DispatchEffect( intiatorFilter, 0.0, data.m_vOrigin, "ParticleEffect", data ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::DoTauntAttack( void ) +{ + if ( !IsTaunting() || !IsAlive() || m_iTauntAttack == TAUNTATK_NONE ) + { + return; + } + + int iTauntAttack = m_iTauntAttack; + m_iTauntAttack = TAUNTATK_NONE; + + if ( iTauntAttack == TAUNTATK_PYRO_HADOUKEN || iTauntAttack == TAUNTATK_SPY_FENCING_SLASH_A || + iTauntAttack == TAUNTATK_SPY_FENCING_SLASH_B || iTauntAttack == TAUNTATK_SPY_FENCING_STAB ) + { + // Pyro Hadouken fireball attack + // Kill all enemies within a small volume in front of the player. + Vector vecForward; + AngleVectors( QAngle(0, m_angEyeAngles[YAW], 0), &vecForward ); + Vector vecCenter = WorldSpaceCenter() + vecForward * 64; + Vector vecSize = Vector(24,24,24); + CBaseEntity *pList[256]; + int count = UTIL_EntitiesInBox( pList, 256, vecCenter - vecSize, vecCenter + vecSize, FL_CLIENT|FL_OBJECT ); + if ( count ) + { + // Launch them up a little + AngleVectors( QAngle(-45, m_angEyeAngles[YAW], 0), &vecForward ); + + for ( int i = 0; i < count; i++ ) + { + // Team damage doesn't prevent us hurting ourself, so we do it manually here + if ( pList[i] == this ) + continue; + + if ( FVisible( pList[i], MASK_SOLID ) == false ) + continue; + + Vector vecPos = WorldSpaceCenter(); + vecPos += (pList[i]->WorldSpaceCenter() - vecPos) * 0.75; + + // Spy taunt does two quick slashes, followed by a killing blow + if ( iTauntAttack == TAUNTATK_SPY_FENCING_SLASH_A || iTauntAttack == TAUNTATK_SPY_FENCING_SLASH_B ) + { + // No physics push so it doesn't push the player out of the range of the stab + pList[i]->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward * 100, vecPos, 25, DMG_SLASH | DMG_PREVENT_PHYSICS_FORCE, TF_DMG_CUSTOM_TAUNTATK_FENCING ) ); + } + else if ( iTauntAttack == TAUNTATK_SPY_FENCING_STAB ) + { + pList[i]->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward * 20000, vecPos, 500.0f, DMG_SLASH, TF_DMG_CUSTOM_TAUNTATK_FENCING ) ); + } + else if ( iTauntAttack == TAUNTATK_PYRO_HADOUKEN ) + { + pList[i]->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward * 25000, vecPos, 500.0f, DMG_BURN | DMG_IGNITE, TF_DMG_CUSTOM_TAUNTATK_HADOUKEN ) ); + } + } + } + + if ( iTauntAttack == TAUNTATK_SPY_FENCING_SLASH_A ) + { + m_iTauntAttack = TAUNTATK_SPY_FENCING_SLASH_B; + m_flTauntAttackTime = gpGlobals->curtime + 0.47; + } + else if ( iTauntAttack == TAUNTATK_SPY_FENCING_SLASH_B ) + { + m_iTauntAttack = TAUNTATK_SPY_FENCING_STAB; + m_flTauntAttackTime = gpGlobals->curtime + 1.73; + } + + if ( tf_debug_damage.GetBool() ) + { + NDebugOverlay::Box( vecCenter, -vecSize, vecSize, 0, 255, 0, 40, 10 ); + } + } + else if ( iTauntAttack == TAUNTATK_SOLDIER_GRENADE_KILL_WORMSIGN ) + { + EmitSound( "Taunt.WormsHHG" ); + m_iTauntAttack = TAUNTATK_SOLDIER_GRENADE_KILL; + m_flTauntAttackTime = gpGlobals->curtime + 2.1; + } + else if ( iTauntAttack == TAUNTATK_SOLDIER_GRENADE_KILL ) + { + matrix3x4_t worldSpace; + MatrixCopy( EntityToWorldTransform(), worldSpace ); + + Vector bonePos; + QAngle boneAngles; + int iRightHand = LookupBone( "bip_hand_r" ); + if ( iRightHand != -1 ) + { + GetBonePosition( iRightHand, bonePos, boneAngles ); + + CPVSFilter filter( bonePos ); + TE_TFExplosion( filter, 0.0f, bonePos, Vector(0,0,1), TF_WEAPON_GRENADELAUNCHER, entindex() ); + + CTakeDamageInfo info( this, this, GetActiveTFWeapon(), vec3_origin, bonePos, 200.f, DMG_BLAST | DMG_USEDISTANCEMOD, TF_DMG_CUSTOM_TAUNTATK_GRENADE, &bonePos ); + CTFRadiusDamageInfo radiusinfo( &info, bonePos, 100.f ); + TFGameRules()->RadiusDamage( radiusinfo ); + } + } + else if ( iTauntAttack == TAUNTATK_SNIPER_ARROW_STAB_IMPALE || iTauntAttack == TAUNTATK_SNIPER_ARROW_STAB_KILL || + iTauntAttack == TAUNTATK_ENGINEER_ARM_IMPALE || iTauntAttack == TAUNTATK_ENGINEER_ARM_KILL || iTauntAttack == TAUNTATK_ENGINEER_ARM_BLEND ) + { + Vector vecForward; + AngleVectors( EyeAngles(), &vecForward ); + Vector vecEnd = EyePosition() + vecForward * 128; + + trace_t tr; + UTIL_TraceLine( EyePosition(), vecEnd, MASK_SOLID & ~CONTENTS_HITBOX, this, COLLISION_GROUP_PLAYER, &tr ); + + if ( tr.fraction < 1.0 ) + { + CBaseEntity *pEnt = tr.m_pEnt; + + if ( pEnt && pEnt->IsPlayer() && pEnt->GetTeamNumber() > LAST_SHARED_TEAM && pEnt->GetTeamNumber() != GetTeamNumber() ) + { + CTFPlayer *pVictim = ToTFPlayer( pEnt ); + + switch ( iTauntAttack ) + { + case TAUNTATK_SNIPER_ARROW_STAB_IMPALE: + case TAUNTATK_ENGINEER_ARM_IMPALE: + if ( pVictim ) + { + // don't stun giants + if ( !pVictim->IsMiniBoss() ) + { + pVictim->m_Shared.StunPlayer( 3.0f, 1.0, TF_STUN_BOTH | TF_STUN_NO_EFFECTS, this ); + } + + if ( iTauntAttack == TAUNTATK_ENGINEER_ARM_IMPALE ) + { + pEnt->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward, pEnt->WorldSpaceCenter(), 1, DMG_BULLET | DMG_PREVENT_PHYSICS_FORCE, TF_DMG_CUSTOM_TAUNTATK_ENGINEER_ARM_KILL ) ); + } + } + break; + + case TAUNTATK_ENGINEER_ARM_BLEND: + pEnt->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward, pEnt->WorldSpaceCenter(), 1, DMG_BULLET | DMG_PREVENT_PHYSICS_FORCE, TF_DMG_CUSTOM_TAUNTATK_ENGINEER_ARM_KILL ) ); + break; + + case TAUNTATK_SNIPER_ARROW_STAB_KILL: + // Launch them up a little + vecForward = (WorldSpaceCenter() - pEnt->WorldSpaceCenter()); + VectorNormalize( vecForward ); + pEnt->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward * 12000, pEnt->WorldSpaceCenter(), 500.0f, DMG_BULLET | DMG_PREVENT_PHYSICS_FORCE, TF_DMG_CUSTOM_TAUNTATK_ARROW_STAB ) ); + break; + + case TAUNTATK_ENGINEER_ARM_KILL: + pEnt->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward * 12000, pEnt->WorldSpaceCenter(), 500.0f, DMG_BLAST, TF_DMG_CUSTOM_TAUNTATK_ENGINEER_ARM_KILL ) ); + break; + } + } + } + + if ( iTauntAttack == TAUNTATK_SNIPER_ARROW_STAB_IMPALE ) + { + m_iTauntAttack = TAUNTATK_SNIPER_ARROW_STAB_KILL; + m_flTauntAttackTime = gpGlobals->curtime + 1.30; + } + else if ( iTauntAttack == TAUNTATK_ENGINEER_ARM_IMPALE ) + { + m_iTauntAttack = TAUNTATK_ENGINEER_ARM_BLEND; + m_flTauntAttackTime = gpGlobals->curtime + 0.05; + m_iTauntAttackCount = 0; + } + else if ( iTauntAttack == TAUNTATK_ENGINEER_ARM_BLEND ) + { + m_iTauntAttack = TAUNTATK_ENGINEER_ARM_BLEND; + m_flTauntAttackTime = gpGlobals->curtime + 0.05; + m_iTauntAttackCount++; + if ( m_iTauntAttackCount == 13 ) + { + m_iTauntAttack = TAUNTATK_ENGINEER_ARM_KILL; + } + } + } + else if ( iTauntAttack == TAUNTATK_HEAVY_EAT ) + { + CTFWeaponBase *pActiveWeapon = m_Shared.GetActiveTFWeapon(); + if ( pActiveWeapon && pActiveWeapon->GetWeaponID() == TF_WEAPON_LUNCHBOX ) + { + CTFLunchBox *pLunchbox = (CTFLunchBox*)pActiveWeapon; + pLunchbox->ApplyBiteEffects( this ); + } + + // Keep eating until the taunt is over + m_iTauntAttack = TAUNTATK_HEAVY_EAT; + m_flTauntAttackTime = gpGlobals->curtime + 1.0; + + // If we're going to finish eating after this bite, say our line + if ( m_flTauntRemoveTime < m_flTauntAttackTime ) + { + if ( IsSpeaking() ) + { + // The player may technically still be speaking even though the actual VO is over and just + // hasn't been cleared yet. We need to force it to end so our next concept can be played. + CMultiplayer_Expresser *pExpresser = GetMultiplayerExpresser(); + if ( pExpresser ) + { + pExpresser->ForceNotSpeaking(); + } + } + + SpeakConceptIfAllowed( MP_CONCEPT_ATE_FOOD ); + } + } + else if ( iTauntAttack == TAUNTATK_HEAVY_RADIAL_BUFF ) + { + Vector vecOrg = GetAbsOrigin(); + + // Find nearby team mates and give them bonus health & crit chance + for ( int i = 0; i < GetTeam()->GetNumPlayers(); i++ ) + { + CTFPlayer *pTeamPlayer = ToTFPlayer( GetTeam()->GetPlayer(i) ); + if ( pTeamPlayer && pTeamPlayer->IsAlive() ) + { + // If they're within the radius, give 'em the buff + if ( (vecOrg - pTeamPlayer->GetAbsOrigin()).LengthSqr() < (1024*1024) ) + { + pTeamPlayer->TakeHealth( 50, DMG_GENERIC ); + pTeamPlayer->m_Shared.AddTempCritBonus( 0.5 ); + + IGameEvent *event = gameeventmanager->CreateEvent( "player_healonhit" ); + if ( event ) + { + event->SetInt( "amount", 50 ); + event->SetInt( "entindex", pTeamPlayer->entindex() ); + event->SetInt( "weapon_def_index", INVALID_ITEM_DEF_INDEX ); + gameeventmanager->FireEvent( event ); + } + } + } + } + } + else if ( iTauntAttack == TAUNTATK_HEAVY_HIGH_NOON ) + { + // Heavy "High Noon" attack + Vector vecForward; + AngleVectors( EyeAngles(), &vecForward ); + Vector vecEnd = EyePosition() + vecForward * 500; + + trace_t tr; + UTIL_TraceLine( EyePosition(), vecEnd, ( MASK_SOLID | CONTENTS_HITBOX ), this, COLLISION_GROUP_PLAYER, &tr ); +// DebugDrawLine( EyePosition(), vecEnd, 0, 0, 255, true, 3.0f ); + + if ( tr.fraction < 1.0 ) + { + CBaseEntity *pEnt = tr.m_pEnt; + + if ( pEnt && pEnt->IsPlayer() && pEnt->GetTeamNumber() > LAST_SHARED_TEAM && pEnt->GetTeamNumber() != GetTeamNumber() ) + { + // Launch them up a little + AngleVectors( QAngle(-45, m_angEyeAngles[YAW], 0), &vecForward ); + pEnt->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward * 25000, WorldSpaceCenter(), 500.0f, DMG_BULLET, TF_DMG_CUSTOM_TAUNTATK_HIGH_NOON ) ); + } + } + } + else if ( iTauntAttack == TAUNTATK_SCOUT_DRINK ) + { + if ( !m_Shared.IsControlStunned() ) + { + // Check for CritBerry flavor + CTFWeaponBase *pActiveWeapon = m_Shared.GetActiveTFWeapon(); + if ( pActiveWeapon && pActiveWeapon->GetWeaponID() == TF_WEAPON_LUNCHBOX ) + { + float flDropDeadTime = ( 100.f / tf_scout_energydrink_consume_rate.GetFloat() ) + 1.f; // Just in case. Normally over in 8 seconds. + + CTFLunchBox *pLunchbox = static_cast< CTFLunchBox* >( pActiveWeapon ); + if ( pLunchbox && pLunchbox->GetLunchboxType() == LUNCHBOX_ADDS_MINICRITS ) + { + m_Shared.AddCond( TF_COND_ENERGY_BUFF, flDropDeadTime ); + } + else + { + m_Shared.AddCond( TF_COND_PHASE, flDropDeadTime ); + + if ( HasTheFlag() ) + { + bool bShouldDrop = true; + + // Always allow teams to hear each other in TD mode + if ( TFGameRules()->IsMannVsMachineMode() && GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + bShouldDrop = false; + } + + if ( bShouldDrop ) + { + DropFlag(); + } + } + } + + SelectLastItem(); + } + } + } + else if ( iTauntAttack == TAUNTATK_SCOUT_GRAND_SLAM ) + { + // Find a player in front of us and knock 'em across the map. + // Same box logic as hadouken & pyro knockback. + Vector vecForward; + AngleVectors( QAngle(0, m_angEyeAngles[YAW], 0), &vecForward ); + Vector vecCenter = WorldSpaceCenter() + vecForward * 64; + Vector vecSize = Vector(24,24,24); + CBaseEntity *pObjects[256]; + int count = UTIL_EntitiesInBox( pObjects, 256, vecCenter - vecSize, vecCenter + vecSize, FL_CLIENT|FL_OBJECT ); + if ( count ) + { + for ( int i=0; i<count; i++ ) + { + // Must be facing whoever we knock back. + Vector vecToTarget; + vecToTarget = pObjects[i]->WorldSpaceCenter() - WorldSpaceCenter(); + VectorNormalize( vecToTarget ); + float flDot = DotProduct( vecForward, vecToTarget ); + if ( flDot < 0.80 ) + continue; + + CTFPlayer *pTarget = ToTFPlayer( pObjects[i] ); + if ( !pTarget ) + continue; + + if ( pTarget->GetTeamNumber() == GetTeamNumber() ) + continue; + + // Do a quick trace and make sure we have LOS. + trace_t tr; + UTIL_TraceLine( WorldSpaceCenter(), pObjects[i]->WorldSpaceCenter(), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_PLAYER, &tr ); + + if ( tr.fraction < 1.0 ) + continue; + + pTarget->SetAbsVelocity( vec3_origin ); + //pTarget->m_Shared.StunPlayer( 8.f, 1.f, TF_STUN_BOTH | TF_STUN_SPECIAL_SOUND ); + pTarget->StunSound( this, TF_STUN_BOTH | TF_STUN_SPECIAL_SOUND ); + pTarget->ApplyPunchImpulseX( RandomInt( 10, 15 ) ); + + AngleVectors( QAngle(-45, m_angEyeAngles[YAW], 0), &vecForward ); + pTarget->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward * 130000, WorldSpaceCenter(), 500.0f, DMG_BULLET, TF_DMG_CUSTOM_TAUNTATK_GRAND_SLAM ) ); + + // Tell the achievement system we swatted someone. + IGameEvent *event = gameeventmanager->CreateEvent( "scout_grand_slam" ); + if ( event ) + { + event->SetInt( "scout_id", GetUserID() ); + event->SetInt( "target_id", pTarget->GetUserID() ); + gameeventmanager->FireEvent( event ); + } + } + } + } + else if ( iTauntAttack == TAUNTATK_MEDIC_HEROIC_TAUNT ) + { + // do these later + m_flTauntAttackTime = gpGlobals->curtime + 3.0f; + m_iTauntAttack = TAUNTATK_MEDIC_RELEASE_DOVES; + + // send a reliable message to make sure the effect happens + CPVSFilter filter( GetAbsOrigin() ); + UserMessageBegin( filter, "PlayerGodRayEffect" ); + WRITE_BYTE( entindex() ); + MessageEnd(); + + EmitSound( "Taunt.MedicHeroic" ); + } + else if ( iTauntAttack == TAUNTATK_MEDIC_RELEASE_DOVES ) + { + // not really a taunt "attack", just a hook to release some doves at the appropriate time + Vector launchSpot = ( WorldSpaceCenter() + GetAbsOrigin() ) / 2.0f; + for( int i=0; i<MEDIC_RELEASE_DOVE_COUNT; ++i ) + { + Vector vecPos = launchSpot + Vector( 0, 0, RandomFloat( -10.0f, 20.0f ) ); + SpawnClientsideFlyingBird( vecPos ); + } + } + else if ( iTauntAttack == TAUNTATK_PYRO_ARMAGEDDON ) + { + Vector origin( GetAbsOrigin() ); + + CPVSFilter filter( origin ); + TE_TFExplosion( filter, 0.0f, origin, Vector( 0.0f, 0.0f, 1.0f ), TF_WEAPON_GRENADELAUNCHER, entindex() ); + + int nRandomPick[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + CUtlVector< CTFPlayer* > vecDamagedPlayers; + const float flRadius = 100.0f; + const float flRadiusSqr = flRadius * flRadius; + + CBaseEntity *pEntity = NULL; + for ( CEntitySphereQuery sphere( origin, flRadius ); (pEntity = sphere.GetCurrentEntity()) != NULL && vecDamagedPlayers.Count() < ARRAYSIZE( nRandomPick ); sphere.NextEntity() ) + { + // Skip players on the same team or who are invuln + CTFPlayer *pPlayer = ToTFPlayer( pEntity ); + if ( !pPlayer || InSameTeam( pPlayer ) || pPlayer->m_Shared.InCond( TF_COND_INVULNERABLE ) ) + continue; + + // CEntitySphereQuery actually does a box test. So we need to make sure the distance is less than the radius first. + Vector vecPos; + pEntity->CollisionProp()->CalcNearestPoint( origin, &vecPos ); + if ( ( origin - vecPos ).LengthSqr() > flRadiusSqr ) + continue; + + // Finally LOS test + trace_t tr; + Vector vecSrc = WorldSpaceCenter(); + Vector vecSpot = pEntity->WorldSpaceCenter(); + CTraceFilterSimple filter( this, COLLISION_GROUP_PROJECTILE ); + UTIL_TraceLine( vecSrc, vecSpot, MASK_SOLID_BRUSHONLY, &filter, &tr ); + + // If we don't trace the whole way to the target, and we didn't hit the target entity, we're blocked + if ( tr.fraction != 1.0 && tr.m_pEnt != pEntity ) + continue; + + vecDamagedPlayers.AddToTail( pPlayer ); + } + + if ( vecDamagedPlayers.Count() ) + { + int nBurnCount = 0; + float fDamage = 400.0f; + + for ( int i = vecDamagedPlayers.Count() - 1; i >= 0; --i ) + { + // Pick a random player + int nRand = RandomInt( 0, i ); + CTFPlayer *pPlayer = vecDamagedPlayers[ nRandomPick[ nRand ] ]; + if ( pPlayer ) + { + bool bBurning = pPlayer->m_Shared.InCond( TF_COND_BURNING ); + + pPlayer->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vec3_origin, origin, fDamage, DMG_PLASMA, ( iTauntAttack == TAUNTATK_PYRO_ARMAGEDDON ) ? TF_DMG_CUSTOM_TAUNTATK_ARMAGEDDON : TF_DMG_CUSTOM_TAUNTATK_ALLCLASS_GUITAR_RIFF, &origin ) ); + + // If they weren't burning before but now they are, count it + if ( !bBurning && pPlayer->m_Shared.InCond( TF_COND_BURNING ) ) + { + nBurnCount++; + } + + // Next choice gets half that amount + fDamage /= 2; + + // The end of the list moves overwrites the one we just picked + nRandomPick[ nRand ] = nRandomPick[ i ]; + } + } + + if ( iTauntAttack == TAUNTATK_PYRO_ARMAGEDDON ) + { + if ( nBurnCount >= 3 ) + { + AwardAchievement( ACHIEVEMENT_TF_PYRO_IGNITE_WITH_RAINBOW ); + } + } + } + + UTIL_ScreenShake( origin, 15.0, 150.0, 0.75f, 500.0f, SHAKE_START ); + + } + else if ( iTauntAttack == TAUNTATK_PYRO_SCORCHSHOT ) + { + CTFWeaponBase *pWeapon = GetActiveTFWeapon(); + if ( pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_FLAREGUN ) + { + CTFWeaponBaseGun *pGun = dynamic_cast< CTFWeaponBaseGun* >( pWeapon ); + if ( pGun ) + { + pGun->FireProjectile( this ); + } + } + } + else if ( iTauntAttack == TAUNTATK_ALLCLASS_GUITAR_RIFF ) + { + // We need to parent this to a target instead of the player because the player changing their camera view can twist the rainbow + CBaseEntity *pTarget = CreateEntityByName( "info_target" ); + if ( pTarget ) + { + DispatchSpawn( pTarget ); + pTarget->SetAbsOrigin( GetAbsOrigin() ); + pTarget->SetAbsAngles( GetAbsAngles() ); + pTarget->SetEFlags( EFL_FORCE_CHECK_TRANSMIT ); + pTarget->SetThink( &BaseClass::SUB_Remove ); + pTarget->SetNextThink( gpGlobals->curtime + 6.0f ); + + CBaseEntity *pGround = GetGroundEntity(); + if ( pGround && pGround->GetMoveType() == MOVETYPE_PUSH ) + { + pTarget->SetParent( pGround ); + } + } + + CBroadcastRecipientFilter filter; + TE_TFParticleEffect( filter, 0.0, "bl_killtaunt", GetAbsOrigin(), GetAbsAngles(), pTarget, PATTACH_ABSORIGIN_FOLLOW ); + EmitSound( "Taunt.GuitarRiff" ); + } + else if ( iTauntAttack == TAUNTATK_MEDIC_INHALE ) + { + int iHealed = TakeHealth( 1, DMG_GENERIC ); + + if ( iHealed > 0 ) + { + CTF_GameStats.Event_PlayerHealedOther( this, iHealed ); + } + + // Keep eating until the taunt is over + if ( m_flTauntInhaleTime > gpGlobals->curtime ) + { + m_iTauntAttack = TAUNTATK_MEDIC_INHALE; + m_flTauntAttackTime = gpGlobals->curtime + 0.1; + } + } + else if ( iTauntAttack == TAUNTATK_MEDIC_UBERSLICE_IMPALE || iTauntAttack == TAUNTATK_MEDIC_UBERSLICE_KILL ) + { + Vector vecForward; + AngleVectors( EyeAngles(), &vecForward ); + Vector vecEnd = EyePosition() + vecForward * 128; + + trace_t tr; + UTIL_TraceLine( EyePosition(), vecEnd, MASK_SOLID & ~CONTENTS_HITBOX, this, COLLISION_GROUP_PLAYER, &tr ); + + if ( tr.fraction < 1.0 ) + { + CBaseEntity *pEnt = tr.m_pEnt; + + if ( pEnt && pEnt->IsPlayer() && pEnt->GetTeamNumber() > LAST_SHARED_TEAM && pEnt->GetTeamNumber() != GetTeamNumber() ) + { + CTFPlayer *pVictim = ToTFPlayer( pEnt ); + + if ( iTauntAttack == TAUNTATK_MEDIC_UBERSLICE_IMPALE ) + { + if ( pVictim ) + { + // don't stun giants + if ( !pVictim->IsMiniBoss() ) + { + pVictim->m_Shared.StunPlayer( 1.5f, 1.0, TF_STUN_BOTH | TF_STUN_NO_EFFECTS, this ); + } + pVictim->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward, WorldSpaceCenter(), 1, DMG_BULLET | DMG_PREVENT_PHYSICS_FORCE, TF_DMG_CUSTOM_TAUNTATK_UBERSLICE ) ); + } + } + else + { + // Launch them up a little + vecForward = (WorldSpaceCenter() - pVictim->WorldSpaceCenter()); + VectorNormalize( vecForward ); + + pVictim->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward * 12000, WorldSpaceCenter(), 500.0f, DMG_BULLET | DMG_PREVENT_PHYSICS_FORCE, TF_DMG_CUSTOM_TAUNTATK_UBERSLICE ) ); + + CWeaponMedigun *pMedigun = (CWeaponMedigun *) Weapon_OwnsThisID( TF_WEAPON_MEDIGUN ); + if ( pMedigun ) + { + pMedigun->AddCharge( 0.5f ); + } + } + } + } + + if ( iTauntAttack == TAUNTATK_MEDIC_UBERSLICE_IMPALE ) + { + m_iTauntAttack = TAUNTATK_MEDIC_UBERSLICE_KILL; + m_flTauntAttackTime = gpGlobals->curtime + 0.75; + } + } + else if ( iTauntAttack == TAUNTATK_DEMOMAN_BARBARIAN_SWING ) + { + Vector vecForward; + AngleVectors( EyeAngles(), &vecForward ); + Vector vecEnd = EyePosition() + vecForward * 128; + + trace_t tr; + UTIL_TraceLine( EyePosition(), vecEnd, MASK_SOLID & ~CONTENTS_HITBOX, this, COLLISION_GROUP_PLAYER, &tr ); + + if ( tr.fraction < 1.0 ) + { + CBaseEntity *pEnt = tr.m_pEnt; + + if ( pEnt && pEnt->IsPlayer() && pEnt->GetTeamNumber() > LAST_SHARED_TEAM && pEnt->GetTeamNumber() != GetTeamNumber() ) + { + vecForward = (WorldSpaceCenter() - pEnt->WorldSpaceCenter()); + VectorNormalize( vecForward ); + pEnt->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward * 12000, WorldSpaceCenter(), 500.0f, DMG_CLUB, TF_DMG_CUSTOM_TAUNTATK_BARBARIAN_SWING ) ); + } + } + } + else if ( iTauntAttack == TAUNTATK_ENGINEER_GUITAR_SMASH ) + { + Vector vecForward; + AngleVectors( EyeAngles(), &vecForward ); + Vector vecEnd = EyePosition() + vecForward * 128; + + trace_t tr; + UTIL_TraceLine( EyePosition(), vecEnd, MASK_SOLID & ~CONTENTS_HITBOX, this, COLLISION_GROUP_PLAYER, &tr ); + + if ( tr.fraction < 1.0 ) + { + CBaseEntity *pEnt = tr.m_pEnt; + + if ( pEnt && pEnt->IsPlayer() && pEnt->GetTeamNumber() > LAST_SHARED_TEAM && pEnt->GetTeamNumber() != GetTeamNumber() ) + { + vecForward = (WorldSpaceCenter() - pEnt->WorldSpaceCenter()); + VectorNormalize( vecForward ); + pEnt->TakeDamage( CTakeDamageInfo( this, this, GetActiveTFWeapon(), vecForward * 12, WorldSpaceCenter(), 500.0f, DMG_CLUB, TF_DMG_CUSTOM_TAUNTATK_ENGINEER_GUITAR_SMASH ) ); + } + } + } + else if ( iTauntAttack == TAUNTATK_SHOW_ITEM ) + { + if ( m_hTauntItem == NULL ) + { + int itemCount = Inventory()->GetItemCount(); + + CUtlVector< CEconItemView * > hatVector; + + for( int i=0; i<itemCount; ++i ) + { + CEconItemView *econItemView = Inventory()->GetItem( i ); + + int iSlot = econItemView->GetStaticData()->GetLoadoutSlot( GetPlayerClass()->GetClassIndex() ); + + if ( iSlot == LOADOUT_POSITION_HEAD ) + { + hatVector.AddToTail( econItemView ); + } + } + + if ( hatVector.Count() > 0 ) + { + int which = RandomInt( 0, hatVector.Count()-1 ); + + CEconItemView *hatView = hatVector[ which ]; + + int iHandBone = LookupBone( "weapon_bone" ); + if ( iHandBone != -1 ) + { + Vector pos; + QAngle angles; + GetBonePosition( iHandBone, pos, angles ); + + pos = Vector( 0, 0, 50.0f ); + + m_hTauntItem = ItemGeneration()->GenerateItemFromScriptData( hatView, pos, angles, NULL ); + + if ( m_hTauntItem != NULL ) + { + m_hTauntItem->AddSolidFlags( FSOLID_NOT_SOLID ); + m_hTauntItem->SetOwnerEntity( this ); + } + } + } + } + } + else if ( iTauntAttack == TAUNTATK_HIGHFIVE_PARTICLE ) + { + if ( m_hHighFivePartner.Get() ) + { + QAngle bodyAngles = BodyAngles(); + bodyAngles.x = 0; + Vector vecForward, vecRight, vecUp; + AngleVectors( bodyAngles, &vecForward, &vecRight, &vecUp ); + + //Msg( "forward: %f %f %f right: %f %f %f up: %f %f %f\n", vecForward.x, vecForward.y, vecForward.z, + // vecRight.x, vecRight.y, vecRight.z, + // vecUp.x, vecUp.y, vecUp.z ); + + Vector vecParticle = GetAbsOrigin() + (vecForward * 30.0f) + (vecRight * -3.0f) + (vecUp * 87.0f); + //Msg( "particle: %f %f %f\n", vecParticle.x, vecParticle.y, vecParticle.z ); + + CEffectData data; + data.m_nHitBox = GetParticleSystemIndex( GetTeamNumber() == TF_TEAM_RED ? "highfive_red" : "highfive_blue" ); + data.m_vOrigin = vecParticle; + data.m_vAngles = vec3_angle; + + CPASFilter filter( data.m_vOrigin ); + filter.SetIgnorePredictionCull( true ); + + te->DispatchEffect( filter, 0.0, data.m_vOrigin, "ParticleEffect", data ); + } + } + else if ( iTauntAttack == TAUNTATK_RPS_PARTICLE ) + { + if ( m_hHighFivePartner.Get() ) + { + bool bInitiatorWin = ( m_iTauntRPSResult / 3 ) == 0; + + // figure out for RPS + // 0:rock 1:paper 2:scissors + int iInitiator = m_iTauntRPSResult % 3; + int iReceiver = ( iInitiator + ( bInitiatorWin ? 2 : 1 ) ) % 3; + + // offset to get the correct particle name + if ( bInitiatorWin ) + { + iInitiator += 3; + } + else + { + iReceiver += 3; + } + + if ( GetTeamNumber() == TF_TEAM_BLUE ) + { + iInitiator += 6; + } + + if ( m_hHighFivePartner->GetTeamNumber() == TF_TEAM_BLUE ) + { + iReceiver += 6; + } + + DispatchRPSEffect( this, s_pszTauntRPSParticleNames[iInitiator] ); + DispatchRPSEffect( m_hHighFivePartner.Get(), s_pszTauntRPSParticleNames[iReceiver] ); + + // setup time to kill the opposing team loser + if ( GetTeamNumber() != m_hHighFivePartner->GetTeamNumber() ) + { + m_iTauntAttack = TAUNTATK_RPS_KILL; + m_flTauntAttackTime = m_flTauntRemoveTime - 1.2f; + } + + IGameEvent *event = gameeventmanager->CreateEvent( "rps_taunt_event" ); + if ( event ) + { + int iInitiatorRPS = m_iTauntRPSResult % 3; + int iReceiverRPS = ( iInitiatorRPS + ( bInitiatorWin ? 2 : 1 ) ) % 3; + + event->SetInt( "winner", bInitiatorWin ? entindex() : m_hHighFivePartner.Get()->entindex() ); + event->SetInt( "winner_rps", bInitiatorWin ? iInitiatorRPS : iReceiverRPS ); + event->SetInt( "loser", bInitiatorWin ? m_hHighFivePartner.Get()->entindex() : entindex() ); + event->SetInt( "loser_rps", bInitiatorWin ? iReceiverRPS : iInitiatorRPS ); + gameeventmanager->FireEvent( event ); + } + } + } + else if ( iTauntAttack == TAUNTATK_RPS_KILL ) + { + if ( m_hHighFivePartner.Get() ) + { + bool bInitiatorWin = ( m_iTauntRPSResult / 3 ) == 0; + + CTFPlayer *pWinner = NULL; + CTFPlayer *pLoser = NULL; + if ( bInitiatorWin ) + { + pWinner = this; + pLoser = m_hHighFivePartner.Get(); + } + else + { + pWinner = m_hHighFivePartner.Get(); + pLoser = this; + } + + // gib the loser + pLoser->m_bSuicideExplode = true; + pLoser->TakeDamage( CTakeDamageInfo( pWinner, pWinner, NULL, 999, DMG_GENERIC, 0 ) ); + } + } + // Particle Being played in VCD instead + //else if ( iTauntAttack == TAUNTATK_FLIP_LAND_PARTICLE ) + //{ + // if ( m_hHighFivePartner.Get() ) + // { + // CEffectData data; + // data.m_nHitBox = GetParticleSystemIndex( GetTeamNumber() == TF_TEAM_RED ? "taunt_flip_land_red" : "taunt_flip_land_blue" ); + // data.m_vOrigin = m_hHighFivePartner.Get()->GetAbsOrigin(); + // data.m_vAngles = m_hHighFivePartner.Get()->GetAbsAngles(); + + // CPASFilter filter( data.m_vOrigin ); + // filter.SetIgnorePredictionCull( true ); + + // te->DispatchEffect( filter, 0.0, data.m_vOrigin, "ParticleEffect", data ); + // } + //} +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTFPlayer::GetSpecialDSP( void ) +{ + int iSpecialDSP = 0; + CALL_ATTRIB_HOOK_INT( iSpecialDSP, special_dsp ); + + return iSpecialDSP; +} + +//----------------------------------------------------------------------------- +// Purpose: Play a one-shot scene +// Input : +// Output : +//----------------------------------------------------------------------------- +float CTFPlayer::PlayScene( const char *pszScene, float flDelay, AI_Response *response, IRecipientFilter *filter ) +{ + MDLCACHE_CRITICAL_SECTION(); + + // 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() ] ); + } + } + + bool bRedTeam = ( GetTeamNumber() == TF_TEAM_RED ); + if ( m_Shared.InCond( TF_COND_DISGUISED ) ) + { + bRedTeam = ( m_Shared.GetDisguiseTeam() == TF_TEAM_RED ); + } + criteriaSet.AppendCriteria( "OnRedTeam", bRedTeam ? "1" : "0" ); + +//============================================================================= +// HPE_BEGIN: +// [msmith] When in training, we kill a lot of guys... a WHOLE LOT. This was +// triggering some response sounds that got very annoying after a while. +//============================================================================= + if ( TFGameRules()->IsInTraining() ) + { + criteriaSet.AppendCriteria( "recentkills", UTIL_VarArgs("%d", 0) ); + } + else + { + criteriaSet.AppendCriteria( "recentkills", UTIL_VarArgs("%d", m_Shared.GetNumKillsInTime(30.0)) ); + } +//============================================================================= +// HPE_END +//============================================================================= + + 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( "cloaked", ( m_Shared.IsStealthed() || m_Shared.InCond( TF_COND_STEALTHED_BLINK ) ) ? "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" ); + + criteriaSet.AppendCriteria( "stunned", m_Shared.IsControlStunned() ? "1" : "0" ); + criteriaSet.AppendCriteria( "snared", m_Shared.IsSnared() ? "1" : "0" ); + criteriaSet.AppendCriteria( "dodging", (m_Shared.InCond( TF_COND_PHASE ) || m_Shared.InCond( TF_COND_PASSTIME_INTERCEPTION )) ? "1" : "0" ); + criteriaSet.AppendCriteria( "doublejumping", (m_Shared.GetAirDash()>0) ? "1" : "0" ); + + switch ( GetTFTeam()->GetRole() ) + { + case TEAM_ROLE_DEFENDERS: + criteriaSet.AppendCriteria( "teamrole", "defense" ); + break; + case TEAM_ROLE_ATTACKERS: + criteriaSet.AppendCriteria( "teamrole", "offense" ); + break; + } + + // 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; + case TF_WPN_TYPE_ITEM1: + criteriaSet.AppendCriteria( "weaponmode", "item1" ); + break; + case TF_WPN_TYPE_ITEM2: + criteriaSet.AppendCriteria( "weaponmode", "item2" ); + break; + } + + if ( WeaponID_IsSniperRifle( pActiveWeapon->GetWeaponID() ) ) + { + CTFSniperRifle *pRifle = dynamic_cast<CTFSniperRifle*>(pActiveWeapon); + if ( pRifle && pRifle->IsZoomed() ) + { + criteriaSet.AppendCriteria( "sniperzoomed", "1" ); + } + } + else if ( pActiveWeapon->GetWeaponID() == TF_WEAPON_MINIGUN ) + { + CTFMinigun *pMinigun = dynamic_cast<CTFMinigun*>(pActiveWeapon); + if ( pMinigun ) + { + criteriaSet.AppendCriteria( "minigunfiretime", UTIL_VarArgs( "%.1f", pMinigun->GetFiringDuration() ) ); + } + } + + CEconItemView *pItem = pActiveWeapon->GetAttributeContainer()->GetItem(); + if ( pItem && pItem->GetItemQuality() != AE_NORMAL ) + { + criteriaSet.AppendCriteria( "item_name", pItem->GetStaticData()->GetDefinitionName() ); + criteriaSet.AppendCriteria( "item_type_name", pItem->GetStaticData()->GetItemTypeName() ); + } + } + + // equipped loadout items + { + static const char* kSlotCriteriaName[CLASS_LOADOUT_POSITION_COUNT] = + { + "loadout_slot_primary", // LOADOUT_POSITION_PRIMARY = 0, + "loadout_slot_secondary", // LOADOUT_POSITION_SECONDARY, + "loadout_slot_melee", // LOADOUT_POSITION_MELEE, + "loadout_slot_utility", // LOADOUT_POSITION_UTILITY, + "loadout_slot_building", // LOADOUT_POSITION_BUILDING, + "loadout_slot_pda", // LOADOUT_POSITION_PDA, + "loadout_slot_pda2", // LOADOUT_POSITION_PDA2, + "loadout_slot_head", // LOADOUT_POSITION_HEAD, + "loadout_slot_misc", // LOADOUT_POSITION_MISC, + "loadout_slot_action", // LOADOUT_POSITION_ACTION, + "loadout_slot_misc2", // LOADOUT_POSITION_MISC2 + "loadout_slot_taunt", // LOADOUT_POSITION_TAUNT + "loadout_slot_taunt2", // LOADOUT_POSITION_TAUNT2 + "loadout_slot_taunt3", // LOADOUT_POSITION_TAUNT3 + "loadout_slot_taunt4", // LOADOUT_POSITION_TAUNT4 + "loadout_slot_taunt5", // LOADOUT_POSITION_TAUNT5 + "loadout_slot_taunt6", // LOADOUT_POSITION_TAUNT6 + "loadout_slot_taunt7", // LOADOUT_POSITION_TAUNT7 + "loadout_slot_taunt8", // LOADOUT_POSITION_TAUNT8 +#ifdef STAGING_ONLY + "loadout_slot_pda3", // LOADOUT_POSITION_PDA3, + //"loadout_slot_misc3", // LOADOUT_POSITION_MISC3 + //"loadout_slot_misc4", // LOADOUT_POSITION_MISC4 + //"loadout_slot_misc5", // LOADOUT_POSITION_MISC5 + //"loadout_slot_misc6", // LOADOUT_POSITION_MISC6 + //"loadout_slot_misc7", // LOADOUT_POSITION_MISC3 + //"loadout_slot_misc8", // LOADOUT_POSITION_MISC4 + //"loadout_slot_misc9", // LOADOUT_POSITION_MISC5 + //"loadout_slot_misc10", // LOADOUT_POSITION_MISC6 + "loadout_slot_building2", // LOADOUT_POSITION_BUILDING2, +#endif // STAGING_ONLY + }; + COMPILE_TIME_ASSERT( ARRAYSIZE(kSlotCriteriaName) == CLASS_LOADOUT_POSITION_COUNT ); + CEconItemView *pItem = NULL; + for ( int i = 0; i < CLASS_LOADOUT_POSITION_COUNT; ++i ) + { + if ( m_EquippedLoadoutItemIndices[i] != LOADOUT_SLOT_USE_BASE_ITEM ) + { + pItem = m_Inventory.GetInventoryItemByItemID( m_EquippedLoadoutItemIndices[i] ); + if ( pItem ) + { + criteriaSet.AppendCriteria( kSlotCriteriaName[i], pItem->GetStaticData()->GetDefinitionName() ); + } + } + } + } + + // 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] ); + + int iVisibleTeam = pTFPlayer->GetTeamNumber(); + if ( pTFPlayer->m_Shared.InCond( TF_COND_DISGUISED ) ) + { + iVisibleTeam = pTFPlayer->m_Shared.GetDisguiseTeam(); + } + + if ( iVisibleTeam != GetTeamNumber() ) + { + criteriaSet.AppendCriteria( "crosshair_enemy", "yes" ); + } + } + } + } + } + + // Previous round win + bool bLoser = ( TFGameRules()->GetPreviousRoundWinners() != TEAM_UNASSIGNED && TFGameRules()->GetPreviousRoundWinners() != GetPrevRoundTeamNum() ); + criteriaSet.AppendCriteria( "LostRound", bLoser ? "1" : "0" ); + + bool bPrevRoundTie = ( ( TFGameRules()->GetRoundsPlayed() > 0 ) && ( TFGameRules()->GetPreviousRoundWinners() == TEAM_UNASSIGNED ) ); + criteriaSet.AppendCriteria( "PrevRoundWasTie", bPrevRoundTie ? "1" : "0" ); + + // Control points + CTriggerAreaCapture *pAreaTrigger = GetControlPointStandingOn(); + 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" ); + } + } + } + } + + bool bIsBonusTime = false; + bool bGameOver = false; + + // Current game state + criteriaSet.AppendCriteria( "GameRound", UTIL_VarArgs( "%d", TFGameRules()->State_Get() ) ); + if ( TFGameRules()->State_Get() == GR_STATE_TEAM_WIN ) + { + criteriaSet.AppendCriteria( "OnWinningTeam", ( TFGameRules()->GetWinningTeam() == GetTeamNumber() ) ? "1" : "0" ); + + bIsBonusTime = ( TFGameRules()->GetStateTransitionTime() > gpGlobals->curtime ); + bGameOver = TFGameRules()->IsGameOver(); + } + + // Number of rounds played + criteriaSet.AppendCriteria( "RoundsPlayed", UTIL_VarArgs( "%d", TFGameRules()->GetRoundsPlayed() ) ); + + // Is this a 6v6 match? + CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch(); + bool bIsComp6v6 = ( pMatch && pMatch->m_eMatchGroup == k_nMatchGroup_Ladder_6v6 ); + criteriaSet.AppendCriteria( "IsComp6v6", bIsComp6v6 ? "1" : "0" ); + + bool bIsCompWinner = m_Shared.InCond( TF_COND_COMPETITIVE_WINNER ); + criteriaSet.AppendCriteria( "IsCompWinner", bIsCompWinner ? "1" : "0" ); + + + // Holiday Taunt + int iSpecialTaunt = 0; + if ( pActiveWeapon ) + { + CALL_ATTRIB_HOOK_INT_ON_OTHER( pActiveWeapon, iSpecialTaunt, special_taunt ); + } + + // only roll random halloween taunt if the active weapon doesn't have special taunt attribute + if ( TFGameRules()->IsHolidayActive( kHoliday_Halloween ) && iSpecialTaunt == 0 ) + { + if ( !TFGameRules()->IsMannVsMachineMode() || ( GetTeamNumber() != TF_TEAM_PVE_INVADERS ) ) + { + if ( pActiveWeapon ) + { + int iRageTaunt = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pActiveWeapon, iRageTaunt, burn_damage_earns_rage ); + CALL_ATTRIB_HOOK_INT_ON_OTHER( pActiveWeapon, iRageTaunt, generate_rage_on_dmg ); + + int iWeaponID = pActiveWeapon->GetWeaponID(); + if ( iWeaponID != TF_WEAPON_LUNCHBOX && !( iRageTaunt && m_Shared.GetRageMeter() >= 100.f ) ) + { + float frand = (float) rand() / VALVE_RAND_MAX; + if ( frand < 0.4f ) + { + criteriaSet.AppendCriteria( "IsHalloweenTaunt", "1" ); + } + } + } + } + } + + if ( TFGameRules()->IsHolidayActive( kHoliday_AprilFools ) && iSpecialTaunt == 0 ) + { + if ( pActiveWeapon ) + { + int iRageTaunt = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pActiveWeapon, iRageTaunt, burn_damage_earns_rage ); + CALL_ATTRIB_HOOK_INT_ON_OTHER( pActiveWeapon, iRageTaunt, generate_rage_on_dmg ); + + int iWeaponID = pActiveWeapon->GetWeaponID(); + if ( iWeaponID != TF_WEAPON_LUNCHBOX && !( iRageTaunt && m_Shared.GetRageMeter() >= 100.f ) ) + { + float frand = (float)rand() / VALVE_RAND_MAX; + if ( frand < 0.8f ) + { + criteriaSet.AppendCriteria( "IsAprilFoolsTaunt", "1" ); + } + } + } + } + + // Force the thriller taunt if we have the thriller condition + if( m_Shared.InCond( TF_COND_HALLOWEEN_THRILLER ) ) + { + criteriaSet.AppendCriteria( "IsHalloweenTaunt", "1" ); + } + + // Only allow these rules if in the holiday + if ( TFGameRules()->IsHolidayActive( kHoliday_HalloweenOrFullMoon ) && iSpecialTaunt == 0 ) + { + // Halloween costume sets + if ( IsRobotCostumeEquipped() ) + { + criteriaSet.AppendCriteria( "IsRobotCostume", "1" ); + } + else if ( IsDemowolf() ) + { + criteriaSet.AppendCriteria( "IsDemowolf", "1" ); + } + else if ( IsFrankenHeavy() ) + { + criteriaSet.AppendCriteria( "IsFrankenHeavy", "1" ); + } + // Single items with response rules + else + { + static CSchemaAttributeDefHandle pAttrDef_AdditionalHalloweenResponseRule( "additional halloween response criteria name" ); + FOR_EACH_VEC_BACK( m_hMyWearables, wbl ) + { + CEconWearable *pWearable = m_hMyWearables[wbl]; + if ( pWearable && pWearable->GetAttributeContainer()->GetItem() ) + { + const char *pszAdditionalResponseRule = NULL; + if ( FindAttribute_UnsafeBitwiseCast<CAttribute_String>( pWearable->GetAttributeContainer()->GetItem(), pAttrDef_AdditionalHalloweenResponseRule, &pszAdditionalResponseRule ) ) + { + criteriaSet.AppendCriteria( pszAdditionalResponseRule, "1" ); + } + } + } + } + + // Zombie could work in addition to any of these + if ( IsZombieCostumeEquipped() ) + { + criteriaSet.AppendCriteria( "IsZombieCostume", "1" ); + } + } + + if ( TFGameRules() && TFGameRules()->GetActiveBoss() && ( TFGameRules()->GetActiveBoss()->GetBossType() == HALLOWEEN_BOSS_MERASMUS ) ) + { + CMerasmus* pMerasmus = assert_cast< CMerasmus* >( TFGameRules()->GetActiveBoss() ); + if ( pMerasmus ) + { + if ( pMerasmus->IsHiding() ) + { + criteriaSet.AppendCriteria( "IsMerasmusHiding", "1" ); + } + } + } + + bool bInHell = false; + if ( TFGameRules() && TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_HIGHTOWER ) && ( TFGameRules()->ArePlayersInHell() == true ) ) + { + bInHell = true; + } + criteriaSet.AppendCriteria( "IsInHell", bInHell ? "1" : "0" ); + + if ( TFGameRules()->IsHolidayActive( kHoliday_HalloweenOrFullMoonOrValentines ) ) + { + if ( IsFairyHeavy() ) + { + criteriaSet.AppendCriteria( "IsFairyHeavy", "1" ); + } + } + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + if ( GetTeamNumber() == TF_TEAM_PVE_DEFENDERS ) + { + criteriaSet.AppendCriteria( "IsMvMDefender", "1" ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTriggerAreaCapture *CTFPlayer::GetControlPointStandingOn( void ) +{ + touchlink_t *root = ( touchlink_t * )GetDataObject( TOUCHLINK ); + if ( root ) + { + for ( touchlink_t *link = root->nextLink; link != root; link = link->nextLink ) + { + CBaseEntity *pTouch = link->entityTouched; + if ( pTouch && pTouch->IsSolidFlagSet( FSOLID_TRIGGER ) && pTouch->IsBSPModel() ) + { + CTriggerAreaCapture *pAreaTrigger = dynamic_cast<CTriggerAreaCapture*>(pTouch); + if ( pAreaTrigger ) + return pAreaTrigger; + } + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Usable by CTFPlayers, not just CTFBots +class CTFPlayertPathCost : public IPathCost +{ +public: + CTFPlayertPathCost( const CTFPlayer *me ) + { + m_me = me; + m_stepHeight = StepHeight; + m_maxJumpHeight = 72.0f; + m_maxDropHeight = 200.0f; + } + + virtual float operator()( CNavArea *baseArea, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const + { + VPROF_BUDGET( "CTFPlayertPathCost::operator()", "NextBot" ); + + CTFNavArea *area = (CTFNavArea *)baseArea; + + if ( fromArea == NULL ) + { + // first area in path, no cost + return 0.0f; + } + else + { + if ( !m_me->IsAreaTraversable( area ) ) + { + return -1.0f; + } + + // don't path through enemy spawn rooms + if ( ( m_me->GetTeamNumber() == TF_TEAM_RED && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) || + ( m_me->GetTeamNumber() == TF_TEAM_BLUE && area->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) ) ) + { + if ( !TFGameRules()->RoundHasBeenWon() ) + { + return -1.0f; + } + } + + // compute distance traveled along path so far + float dist; + + if ( ladder ) + { + dist = ladder->m_length; + } + else if ( length > 0.0 ) + { + dist = length; + } + else + { + dist = ( area->GetCenter() - fromArea->GetCenter() ).Length(); + } + + // check height change + float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange( area ); + + if ( deltaZ >= m_stepHeight ) + { + if ( deltaZ >= m_maxJumpHeight ) + { + // too high to reach + return -1.0f; + } + + // jumping is slower than flat ground + const float jumpPenalty = 2.0f; + dist *= jumpPenalty; + } + else if ( deltaZ < -m_maxDropHeight ) + { + // too far to drop + return -1.0f; + } + + float cost = dist + fromArea->GetCostSoFar(); + + return cost; + } + } + + const CTFPlayer *m_me; + float m_stepHeight; + float m_maxJumpHeight; + float m_maxDropHeight; +}; + +//----------------------------------------------------------------------------- +// Given a vector of points, return the point we can actually travel to the quickest (requires a nav mesh) +CTeamControlPoint *CTFPlayer::SelectClosestControlPointByTravelDistance( CUtlVector< CTeamControlPoint * > *pointVector ) const +{ + if ( !pointVector || pointVector->Count() == 0 ) + { + return NULL; + } + + if ( GetLastKnownArea() == NULL ) + { + return NULL; + } + + CTeamControlPoint *closestPoint = NULL; + float closestPointTravelRange = FLT_MAX; + CTFPlayertPathCost cost( this ); + + for( int i=0; i<pointVector->Count(); ++i ) + { + CTeamControlPoint *point = pointVector->Element(i); + + if ( IsBot() && point->ShouldBotsIgnore() ) + continue; + + float travelRange = NavAreaTravelDistance( GetLastKnownArea(), TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() ), cost ); + + if ( travelRange >= 0.0 && travelRange < closestPointTravelRange ) + { + closestPoint = point; + closestPointTravelRange = travelRange; + } + } + + return closestPoint; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::CanHearAndReadChatFrom( CBasePlayer *pPlayer ) +{ + // always can hear coach + if ( m_hCoach && m_hCoach == pPlayer ) + return true; + + // always can hear student + if ( m_hStudent && m_hStudent == pPlayer ) + return true; + + // 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; + + // Always allow teams to hear each other in TD mode + if ( TFGameRules()->IsMannVsMachineMode() ) + return ( GetTeamNumber() == pPlayer->GetTeamNumber() ); + + if ( pPlayer->m_lifeState != LIFE_ALIVE && m_lifeState == LIFE_ALIVE ) + { + // 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; + + if ( !tf_gravetalk.GetBool() ) + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::CanBeAutobalanced() +{ + if ( DuelMiniGame_IsInDuel( this ) ) + return false; + + if ( IsBot() ) + return false; + + if ( IsCoaching() ) + return false; + + if ( GetCoach() ) + return false; + + if ( m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) ) + return false; + + if ( m_Shared.InCond( TF_COND_HALLOWEEN_KART ) ) + return false; + + 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; + } + + if ( iConcept == MP_CONCEPT_PLAYER_ASK_FOR_BALL ) + { + if ( !SayAskForBall() ) + return false; + } + + // Save the current concept. + m_iCurrentConcept = iConcept; + + if ( m_Shared.InCond( TF_COND_DISGUISED ) && !filter && ( iConcept != MP_CONCEPT_KILLED_PLAYER ) ) + { + 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 + { + if ( IsPlayerClass( TF_CLASS_SOLDIER ) && !filter && iConcept == MP_CONCEPT_PLAYER_MEDIC ) + { + // Prevent the medic call+effect when we have the weapon_blocks_healing attribute + CTFWeaponBase *pTFWeapon = GetActiveTFWeapon(); + if ( pTFWeapon ) + { + int iBlockHealing = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pTFWeapon, iBlockHealing, weapon_blocks_healing ); + if ( iBlockHealing ) + return false; + } + } + + // 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 ); + + float flTimeSinceAllowedVoice = gpGlobals->curtime - m_flNextVoiceCommandTime; + + // if its longer than 5 seconds, reset the counter + if ( flTimeSinceAllowedVoice > 5.0f ) + { + m_iVoiceSpamCounter = 0; + } + // if its less than a second past the allowed time, player is spamming + else if ( flTimeSinceAllowedVoice < 1.0f ) + { + m_iVoiceSpamCounter++; + } + + m_flNextVoiceCommandTime = gpGlobals->curtime + MIN( GetSceneDuration( pszScenePlayed ), tf_max_voice_speak_delay.GetFloat() ); + + if ( m_iVoiceSpamCounter > 0 ) + { + m_flNextVoiceCommandTime += m_iVoiceSpamCounter * 0.5f; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::WantsLagCompensationOnEntity( const CBasePlayer *pPlayer, const CUserCmd *pCmd, const CBitVec<MAX_EDICTS> *pEntityTransmitBits ) const +{ + bool bIsMedic = false; + + //Do Lag comp on medics trying to heal team mates. + if ( IsPlayerClass( TF_CLASS_MEDIC ) == true ) + { + bIsMedic = true; + + if ( pPlayer->GetTeamNumber() == GetTeamNumber() ) + { + CWeaponMedigun *pWeapon = dynamic_cast <CWeaponMedigun*>( GetActiveWeapon() ); + + if ( pWeapon && pWeapon->GetHealTarget() ) + { + if ( pWeapon->GetHealTarget() == pPlayer ) + return true; + else + return false; + } + } + } + + if ( pPlayer->GetTeamNumber() == GetTeamNumber() && bIsMedic == false ) + return false; + + // If this entity hasn't been transmitted to us and acked, then don't bother lag compensating it. + if ( pEntityTransmitBits && !pEntityTransmitBits->Get( pPlayer->entindex() ) ) + return false; + + const Vector &vMyOrigin = GetAbsOrigin(); + const Vector &vHisOrigin = pPlayer->GetAbsOrigin(); + + // get max distance player could have moved within max lag compensation time, + // multiply by 1.5 to to avoid "dead zones" (sqrt(2) would be the exact value) + float maxDistance = 1.5 * pPlayer->MaxSpeed() * sv_maxunlag.GetFloat(); + + // If the player is within this distance, lag compensate them in case they're running past us. + if ( vHisOrigin.DistTo( vMyOrigin ) < maxDistance ) + return true; + + // If their origin is not within a 45 degree cone in front of us, no need to lag compensate. + Vector vForward; + AngleVectors( pCmd->viewangles, &vForward ); + + Vector vDiff = vHisOrigin - vMyOrigin; + VectorNormalize( vDiff ); + + float flCosAngle = 0.707107f; // 45 degree angle + if ( vForward.Dot( vDiff ) < flCosAngle ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::SpeakWeaponFire( int iCustomConcept ) +{ + if ( iCustomConcept == MP_CONCEPT_NONE ) + { + if ( m_flNextSpeakWeaponFire > gpGlobals->curtime ) + return; + + iCustomConcept = MP_CONCEPT_FIREWEAPON; + } + + m_flNextSpeakWeaponFire = gpGlobals->curtime + 5; + + 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 ) +{ + m_flNextSpeakWeaponFire = gpGlobals->curtime; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +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 result = SpeakConcept( response, iConcept ); + + if ( result ) + { + // Apply contexts + if ( response.IsApplyContextToWorld() ) + { + CBaseEntity *pEntity = CBaseEntity::Instance( engine->PEntityOfEntIndex( 0 ) ); + if ( pEntity ) + { + pEntity->AddContext( response.GetContext() ); + } + } + else + { + AddContext( response.GetContext() ); + } + + const char *szResponse = response.GetResponsePtr(); + Q_strncpy( chSceneBuffer, szResponse, numSceneBufferBytes ); + return true; + } + return false; +} + +//----------------------------------------------------------------------------- +// 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: Exclude during win state +//----------------------------------------------------------------------------- +void CTFPlayer::AwardAchievement( int iAchievement, int iCount ) +{ + if ( TFGameRules()->State_Get() >= GR_STATE_TEAM_WIN ) + { + // allow the Helltower loot island achievement during the bonus time + if ( iAchievement != ACHIEVEMENT_TF_HALLOWEEN_HELLTOWER_SKULL_ISLAND_REWARD ) + { + // reject in endround + return; + } + } + + BaseClass::AwardAchievement( iAchievement, iCount ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +// Debugging Stuff +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, pPlayer->GetActiveTFWeapon() ); +} +static ConCommand cc_IgnitePlayer( "tf_ignite_player", IgnitePlayer, "Sets you on fire", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TestVCD( const CCommand &args ) +{ + CBaseEntity *pEntity = FindPickerEntity( UTIL_GetCommandClient() ); + if ( pEntity && pEntity->IsPlayer() ) + { + CTFPlayer *pPlayer = ToTFPlayer( pEntity ); + if ( pPlayer ) + { + if ( args.ArgC() >= 2 ) + { + InstancedScriptedScene( pPlayer, args[1], NULL, 0.0f, false, NULL, true ); + } + else + { + InstancedScriptedScene( pPlayer, "scenes/heavy_test.vcd", NULL, 0.0f, false, NULL, true ); + } + } + } +} +static ConCommand tf_testvcd( "tf_testvcd", TestVCD, "Run a vcd on the player currently under your crosshair. Optional parameter is the .vcd name (default is 'scenes/heavy_test.vcd')", FCVAR_CHEAT ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TestRR( const CCommand &args ) +{ + if ( args.ArgC() < 2 ) + { + Msg("No concept specified. Format is tf_testrr <concept>\n"); + return; + } + + CBaseEntity *pEntity = NULL; + const char *pszConcept = args[1]; + + if ( args.ArgC() == 3 ) + { + pszConcept = args[2]; + pEntity = UTIL_PlayerByName( args[1] ); + } + + if ( !pEntity || !pEntity->IsPlayer() ) + { + pEntity = FindPickerEntity( UTIL_GetCommandClient() ); + if ( !pEntity || !pEntity->IsPlayer() ) + { + pEntity = ToTFPlayer( UTIL_GetCommandClient() ); + } + } + + if ( pEntity && pEntity->IsPlayer() ) + { + CTFPlayer *pPlayer = ToTFPlayer( pEntity ); + if ( pPlayer ) + { + int iConcept = GetMPConceptIndexFromString( pszConcept ); + if ( iConcept != MP_CONCEPT_NONE ) + { + pPlayer->SpeakConceptIfAllowed( iConcept ); + } + else + { + Msg( "Attempted to speak unknown multiplayer concept: %s\n", pszConcept ); + } + } + } +} +static ConCommand tf_testrr( "tf_testrr", TestRR, "Force the player under your crosshair to speak a response rule concept. Format is tf_testrr <concept>, or tf_testrr <player name> <concept>", FCVAR_CHEAT ); + +#ifdef _DEBUG +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" ); + } + } + } +} +#endif // _DEBUG + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::SetPowerplayEnabled( bool bOn ) +{ + if ( bOn ) + { + m_bInPowerPlay = true; + m_Shared.RecalculateChargeEffects(); + m_Shared.Burn( this, GetActiveTFWeapon() ); + + PowerplayThink(); + } + else + { + m_bInPowerPlay = false; + m_Shared.RemoveCond( TF_COND_BURNING ); + m_Shared.RecalculateChargeEffects(); + } + return true; +} + +uint64 powerplaymask = 0xFAB2423BFFA352AFull; +uint64 powerplay_ids[] = +{ + 76561197960435530ull ^ powerplaymask, + 76561197960265731ull ^ powerplaymask, + 76561197960265749ull ^ powerplaymask, + 76561197962783665ull ^ powerplaymask, + 76561197991390878ull ^ powerplaymask, + 76561197979187556ull ^ powerplaymask, + 76561197960269040ull ^ powerplaymask, + 76561197968459473ull ^ powerplaymask, + 76561197989728462ull ^ powerplaymask, +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::PlayerHasPowerplay( void ) +{ + if ( !engine->IsClientFullyAuthenticated( edict() ) ) + return false; + +#if !defined(NO_STEAM) + CSteamID steamIDForPlayer; + if ( GetSteamID( &steamIDForPlayer ) != false ) + { + // Allow beta/dev players in staging + if ( ( engine->GetAppID() == 810 || engine->GetAppID() == 826 ) && + ( steamIDForPlayer.GetEUniverse() == k_EUniverseBeta || steamIDForPlayer.GetEUniverse() == k_EUniverseDev ) ) + return true; + + for ( int i = 0; i < ARRAYSIZE(powerplay_ids); i++ ) + { + if ( steamIDForPlayer.ConvertToUint64() == (powerplay_ids[i] ^ powerplaymask) ) + return true; + } + } +#endif + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::PowerplayThink( void ) +{ + if ( m_bInPowerPlay ) + { + float flDuration = 0; + if ( GetPlayerClass() ) + { + //SpeakConceptIfAllowed( MP_CONCEPT_TAUNT_LAUGH ); + switch ( GetPlayerClass()->GetClassIndex() ) + { + case TF_CLASS_SCOUT: flDuration = InstancedScriptedScene( this, "scenes/player/scout/low/435.vcd", NULL, 0.0f, false, NULL, true ); break; // laughlong02 + case TF_CLASS_SNIPER: flDuration = InstancedScriptedScene( this, "scenes/player/sniper/low/1674.vcd", NULL, 0.0f, false, NULL, true ); break; // laughlong01 + case TF_CLASS_SOLDIER: flDuration = InstancedScriptedScene( this, "scenes/player/soldier/low/1346.vcd", NULL, 0.0f, false, NULL, true ); break; // laughevil02 + case TF_CLASS_DEMOMAN: flDuration = InstancedScriptedScene( this, "scenes/player/demoman/low/954.vcd", NULL, 0.0f, false, NULL, true ); break; // laughlong02 + case TF_CLASS_MEDIC: flDuration = InstancedScriptedScene( this, "scenes/player/medic/low/608.vcd", NULL, 0.0f, false, NULL, true ); break; // laughlong02 + case TF_CLASS_HEAVYWEAPONS: flDuration = InstancedScriptedScene( this, "scenes/player/heavy/low/270.vcd", NULL, 0.0f, false, NULL, true ); break; // laughlong01 + case TF_CLASS_PYRO: flDuration = InstancedScriptedScene( this, "scenes/player/pyro/low/1485.vcd", NULL, 0.0f, false, NULL, true ); break; // laughlong01 + case TF_CLASS_SPY: flDuration = InstancedScriptedScene( this, "scenes/player/spy/low/1312.vcd", NULL, 0.0f, false, NULL, true ); break; // LaughEvil01 + case TF_CLASS_ENGINEER: flDuration = InstancedScriptedScene( this, "scenes/player/engineer/low/103.vcd", NULL, 0.0f, false, NULL, true ); break; // laughlong01 + } + } + + SetContextThink( &CTFPlayer::PowerplayThink, gpGlobals->curtime + flDuration + RandomFloat( 2, 5 ), "TFPlayerLThink" ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::ShouldAnnounceAchievement( void ) +{ + if ( IsPlayerClass( TF_CLASS_SPY ) ) + { + if ( m_Shared.IsStealthed() || + m_Shared.InCond( TF_COND_DISGUISED ) || + m_Shared.InCond( TF_COND_DISGUISING ) ) + { + return false; + } + } + + return BaseClass::ShouldAnnounceAchievement(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +medigun_charge_types CTFPlayer::GetChargeEffectBeingProvided( void ) +{ + if ( !IsPlayerClass(TF_CLASS_MEDIC) ) + return MEDIGUN_CHARGE_INVALID; + + if ( !IsBot() ) + { + INetChannelInfo *pNetChanInfo = engine->GetPlayerNetInfo( entindex() ); + if ( !pNetChanInfo || pNetChanInfo->IsTimingOut() ) + return MEDIGUN_CHARGE_INVALID; + + float flUberDuration = weapon_medigun_chargerelease_rate.GetFloat(); + + // Return invalid when the medic hasn't sent a usercommand in awhile + if ( GetTimeSinceLastUserCommand() > flUberDuration + 1.f ) + return MEDIGUN_CHARGE_INVALID; + + // Prevent an exploit where clients invalidate tickcount - + // which causes their think functions to shut down + if ( GetTimeSinceLastThink() > flUberDuration ) + return MEDIGUN_CHARGE_INVALID; + } + + CTFWeaponBase *pWpn = GetActiveTFWeapon(); + if ( !pWpn ) + return MEDIGUN_CHARGE_INVALID; + + CWeaponMedigun *pMedigun = dynamic_cast<CWeaponMedigun*>(pWpn); + if ( pMedigun && pMedigun->IsReleasingCharge() ) + return pMedigun->GetChargeType(); + + return MEDIGUN_CHARGE_INVALID; +} + +//----------------------------------------------------------------------------- +// Purpose: ACHIEVEMENT_TF_MEDIC_ASSIST_HEAVY handler +//----------------------------------------------------------------------------- +void CTFPlayer::HandleAchievement_Medic_AssistHeavy( CTFPlayer *pPunchVictim ) +{ + if ( !pPunchVictim ) + { + // reset + m_aPunchVictims.RemoveAll(); + return; + } + + // we assisted punching this guy, while invuln + + // if this is a new unique punch victim + if ( m_aPunchVictims.Find( pPunchVictim ) == m_aPunchVictims.InvalidIndex() ) + { + m_aPunchVictims.AddToTail( pPunchVictim ); + + if ( m_aPunchVictims.Count() >= 2 ) + { + AwardAchievement( ACHIEVEMENT_TF_MEDIC_ASSIST_HEAVY ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: ACHIEVEMENT_TF_PYRO_KILL_FROM_BEHIND handler +//----------------------------------------------------------------------------- +void CTFPlayer::HandleAchievement_Pyro_BurnFromBehind( CTFPlayer *pBurner ) +{ + if ( !pBurner ) + { + // reset + m_aBurnFromBackAttackers.RemoveAll(); + return; + } + + if ( m_aBurnFromBackAttackers.Find( pBurner ) == m_aBurnFromBackAttackers.InvalidIndex() ) + { + m_aBurnFromBackAttackers.AddToTail( pBurner ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ResetPerRoundStats( void ) +{ + m_Shared.ResetArenaNumChanges(); + BaseClass::ResetPerRoundStats(); +} + +//----------------------------------------------------------------------------- +// Purpose: Steam has just notified us that the player changed his inventory +//----------------------------------------------------------------------------- +void CTFPlayer::InventoryUpdated( CPlayerInventory *pInventory ) +{ + m_Shared.SetLoadoutUnavailable( false ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::SaveLastWeaponSlot( void ) +{ + if( !m_bRememberLastWeapon && !m_bRememberActiveWeapon ) + return; + + if ( GetLastWeapon() ) + { + if ( !m_bSwitchedClass ) + { + if ( !m_bRememberLastWeapon ) + { + m_iLastWeaponSlot = 0; + + CTFWeaponBase *pWpn = m_Shared.GetActiveTFWeapon(); + if ( pWpn && m_iLastWeaponSlot == pWpn->GetSlot() ) + { + m_iLastWeaponSlot = (m_iLastWeaponSlot == 0) ? 1 : 0; + } + } + else + { + m_iLastWeaponSlot = GetLastWeapon()->GetSlot(); + + if ( !m_bRememberActiveWeapon ) + { + if ( m_iLastWeaponSlot == 0 && m_Shared.GetActiveTFWeapon() ) + { + m_iLastWeaponSlot = m_Shared.GetActiveTFWeapon()->GetSlot(); + } + } + } + } + else + { + m_iLastWeaponSlot = 1; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveAllWeapons() +{ + // Base class RemoveAllWeapons() doesn't remove them properly. + // (doesn't call unequip, or remove immediately. Results in incorrect provision + // state for players over round restarts, because players have 2x weapon entities) + ClearActiveWeapon(); + for (int i = 0; i < MAX_WEAPONS; i++) + { + CBaseCombatWeapon *pWpn = m_hMyWeapons[i]; + if ( pWpn ) + { + Weapon_Detach( pWpn ); + UTIL_Remove( pWpn ); + } + } + + m_Shared.RemoveDisguiseWeapon(); + + // Remove all our wearables + for ( int wbl = m_hMyWearables.Count()-1; wbl >= 0; wbl-- ) + { + CEconWearable *pWearable = m_hMyWearables[wbl]; + if ( pWearable ) + { + RemoveWearable( pWearable ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::Weapon_Equip( CBaseCombatWeapon *pWeapon ) +{ + BaseClass::Weapon_Equip( pWeapon ); + + // Drop the flag if we're no longer supposed to be able to carry it + // This can happen if we're carrying a flag and then pick up a weapon + // that disallows flag carrying (ex. Rocket Jumper, Sticky Jumper) + if ( !IsAllowedToPickUpFlag() && HasTheFlag() ) + { + DropFlag(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::OnAchievementEarned( int iAchievement ) +{ + BaseClass::OnAchievementEarned( iAchievement ); + + SpeakConceptIfAllowed( MP_CONCEPT_ACHIEVEMENT_AWARD ); +} + +//----------------------------------------------------------------------------- +// Purpose: Handles USE keypress +//----------------------------------------------------------------------------- +void CTFPlayer::PlayerUse ( void ) +{ + if ( tf_allow_player_use.GetBool() == false ) + { + if ( !IsObserver() && !IsInCommentaryMode() ) + { + return; + } + } + + BaseClass::PlayerUse(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputRoundSpawn( inputdata_t &inputdata ) +{ + CTFSpellBook *pSpellBook = dynamic_cast< CTFSpellBook* >( GetEntityForLoadoutSlot( LOADOUT_POSITION_ACTION ) ); + if ( pSpellBook ) + { + // Take away players' spells on round restart + pSpellBook->ClearSpell(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::Internal_HandleMapEvent( inputdata_t &inputdata ) +{ + if ( FStrEq( "mvm_mannhattan", STRING( gpGlobals->mapname ) ) ) + { + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + if ( GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + if ( FStrEq( inputdata.value.String(), "banana" ) ) + { + CTFPlayer *pRecentDamager = TFGameRules()->GetRecentDamager( this, 0, 5.0 ); + if ( pRecentDamager && ( pRecentDamager->GetTeamNumber() == TF_TEAM_PVE_DEFENDERS ) ) + { + pRecentDamager->AwardAchievement( ACHIEVEMENT_TF_MVM_MAPS_MANNHATTAN_MYSTERY ); + } + } + else if ( FStrEq( inputdata.value.String(), "pit" ) ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_mannhattan_pit" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + } + } + } + } + else if ( FStrEq( "mvm_rottenburg", STRING( gpGlobals->mapname ) ) ) + { + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + if ( GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + if ( FStrEq( inputdata.value.String(), "pit" ) ) + { + CTFPlayer *pRecentDamager = TFGameRules()->GetRecentDamager( this, 0, 5.0 ); + if ( pRecentDamager && ( pRecentDamager->GetTeamNumber() == TF_TEAM_PVE_DEFENDERS ) ) + { + pRecentDamager->AwardAchievement( ACHIEVEMENT_TF_MVM_MAPS_ROTTENBURG_PIT_GRIND ); + } + } + } + } + } + + BaseClass::Internal_HandleMapEvent( inputdata ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputIgnitePlayer( inputdata_t &inputdata ) +{ + if ( FStrEq( "sd_doomsday", STRING( gpGlobals->mapname ) ) ) + { + CTFPlayer *pRecentDamager = TFGameRules()->GetRecentDamager( this, 0, 5.0 ); + if ( pRecentDamager && ( pRecentDamager->GetTeamNumber() != GetTeamNumber() ) ) + { + pRecentDamager->AwardAchievement( ACHIEVEMENT_TF_MAPS_DOOMSDAY_PUSH_INTO_EXHAUST ); + } + } + + m_Shared.Burn( this, NULL ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputSetCustomModel( inputdata_t &inputdata ) +{ + m_PlayerClass.SetCustomModel( inputdata.value.String() ); + UpdateModel(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputSetCustomModelRotation( inputdata_t &inputdata ) +{ + Vector vecTmp; + inputdata.value.Vector3D( vecTmp ); + QAngle angTmp(vecTmp.x, vecTmp.y, vecTmp.z); + m_PlayerClass.SetCustomModelRotation( angTmp ); + InvalidatePhysicsRecursive( ANGLES_CHANGED ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputClearCustomModelRotation( inputdata_t &inputdata ) +{ + m_PlayerClass.ClearCustomModelRotation(); + InvalidatePhysicsRecursive( ANGLES_CHANGED ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputSetCustomModelOffset( inputdata_t &inputdata ) +{ + Vector vecTmp; + inputdata.value.Vector3D( vecTmp ); + m_PlayerClass.SetCustomModelOffset( vecTmp ); + InvalidatePhysicsRecursive( POSITION_CHANGED ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputSetCustomModelRotates( inputdata_t &inputdata ) +{ + m_PlayerClass.SetCustomModelRotates( inputdata.value.Bool() ); + InvalidatePhysicsRecursive( ANGLES_CHANGED ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputSetCustomModelVisibleToSelf( inputdata_t &inputdata ) +{ + m_PlayerClass.SetCustomModelVisibleToSelf( inputdata.value.Bool() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputSetForcedTauntCam( inputdata_t &inputdata ) +{ + m_nForceTauntCam = inputdata.value.Int(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputExtinguishPlayer( inputdata_t &inputdata ) +{ + if ( m_Shared.InCond( TF_COND_BURNING ) ) + { + EmitSound( "TFPlayer.FlameOut" ); + m_Shared.RemoveCond( TF_COND_BURNING ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputTriggerLootIslandAchievement( inputdata_t &inputdata ) +{ + if ( TFGameRules() && TFGameRules()->IsHolidayActive( kHoliday_Halloween ) ) + { + if ( TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_VIADUCT ) ) + { + AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_LOOT_ISLAND ); + } + else if ( TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_LAKESIDE ) ) + { + AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_MERASMUS_COLLECT_LOOT ); + } + else if ( TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_HIGHTOWER ) ) + { + // the other maps require a min number of players before the boss appears but this one doesn't + // so we need to have at least 1 player on the enemy team before granting the achievement + CUtlVector< CTFPlayer* > playerVector; + CollectHumanPlayers( &playerVector, ( GetTeamNumber() == TF_TEAM_RED ) ? TF_TEAM_BLUE : TF_TEAM_RED ); + if ( playerVector.Count() >= 1 ) + { + AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_HELLTOWER_SKULL_ISLAND_REWARD ); + + IGameEvent *pEvent = gameeventmanager->CreateEvent( "escape_hell" ); + if ( pEvent ) + { + pEvent->SetInt( "player", GetUserID() ); + gameeventmanager->FireEvent( pEvent, true ); + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputTriggerLootIslandAchievement2( inputdata_t &inputdata ) +{ + // nothing here yet + +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputRollRareSpell( inputdata_t &inputdata ) +{ + CTFSpellBook *pSpellBook = dynamic_cast< CTFSpellBook* >( GetEntityForLoadoutSlot( LOADOUT_POSITION_ACTION ) ); + if ( pSpellBook ) + { + pSpellBook->RollNewSpell( 1 ); + + CSingleUserRecipientFilter user( this ); + EmitSound( user, entindex(), "Halloween.Merasmus_TP_In" ); + } + + IGameEvent *pEvent = gameeventmanager->CreateEvent( "cross_spectral_bridge" ); + if ( pEvent ) + { + pEvent->SetInt( "player", GetUserID() ); + gameeventmanager->FireEvent( pEvent, true ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputBleedPlayer( inputdata_t &inputdata ) +{ + m_Shared.MakeBleed( this, GetActiveTFWeapon(), inputdata.value.Float() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Adds this damager to the history list of people who damaged player +//----------------------------------------------------------------------------- +void CAchievementData::AddDamagerToHistory( EHANDLE hDamager ) +{ + if ( !hDamager ) + return; + + EntityHistory_t newHist; + newHist.hEntity = hDamager; + newHist.flTimeDamage = gpGlobals->curtime; + aDamagers.InsertHistory( newHist ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns whether or not pDamager has damaged the player in the time specified +//----------------------------------------------------------------------------- +bool CAchievementData::IsDamagerInHistory( CBaseEntity *pDamager, float flTimeWindow ) +{ + for ( int i = 0; i < aDamagers.Count(); i++ ) + { + if ( ( gpGlobals->curtime - aDamagers[i].flTimeDamage ) > flTimeWindow ) + return false; + + if ( aDamagers[i].hEntity == pDamager ) + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the number of players who've damaged us in the time specified +//----------------------------------------------------------------------------- +int CAchievementData::CountDamagersWithinTime( float flTime ) +{ + int iCount = 0; + for ( int i = 0; i < aDamagers.Count(); i++ ) + { + if ( gpGlobals->curtime - aDamagers[i].flTimeDamage < flTime ) + { + iCount++; + } + } + + return iCount; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAchievementData::AddTargetToHistory( EHANDLE hTarget ) +{ + if ( !hTarget ) + return; + + EntityHistory_t newHist; + newHist.hEntity = hTarget; + newHist.flTimeDamage = gpGlobals->curtime; + aTargets.InsertHistory( newHist ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CAchievementData::IsTargetInHistory( CBaseEntity *pTarget, float flTimeWindow ) +{ + for ( int i = 0; i < aTargets.Count(); i++ ) + { + if ( ( gpGlobals->curtime - aTargets[i].flTimeDamage ) > flTimeWindow ) + return false; + + if ( aTargets[i].hEntity == pTarget ) + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CAchievementData::CountTargetsWithinTime( float flTime ) +{ + int iCount = 0; + for ( int i = 0; i < aTargets.Count(); i++ ) + { + if ( ( gpGlobals->curtime - aTargets[i].flTimeDamage ) < flTime ) + { + iCount++; + } + } + + return iCount; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAchievementData::DumpDamagers( void ) +{ + Msg("Damagers:\n"); + for ( int i = 0; i < aDamagers.Count(); i++ ) + { + if ( aDamagers[i].hEntity ) + { + if ( aDamagers[i].hEntity->IsPlayer() ) + { + Msg(" %s : at %.2f (%.2f ago)\n", ToTFPlayer(aDamagers[i].hEntity)->GetPlayerName(), aDamagers[i].flTimeDamage, gpGlobals->curtime - aDamagers[i].flTimeDamage ); + } + else + { + Msg(" %s : at %.2f (%.2f ago)\n", aDamagers[i].hEntity->GetDebugName(), aDamagers[i].flTimeDamage, gpGlobals->curtime - aDamagers[i].flTimeDamage ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Adds this attacker to the history of people who damaged this player +//----------------------------------------------------------------------------- +void CAchievementData::AddDamageEventToHistory( EHANDLE hAttacker, float flDmgAmount /*= 0.f*/ ) +{ + if ( !hAttacker ) + return; + + EntityDamageHistory_t newHist; + newHist.hEntity = hAttacker; + newHist.flTimeDamage = gpGlobals->curtime; + newHist.nDamageAmount = flDmgAmount; + aDamageEvents.InsertHistory( newHist ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns whether or not pEntity has damaged the player in the time specified +//----------------------------------------------------------------------------- +bool CAchievementData::IsEntityInDamageEventHistory( CBaseEntity *pEntity, float flTimeWindow ) +{ + for ( int i = 0; i < aDamageEvents.Count(); i++ ) + { + if ( aDamageEvents[i].hEntity != pEntity ) + continue; + + // Sorted + if ( ( gpGlobals->curtime - aDamageEvents[i].flTimeDamage ) > flTimeWindow ) + break; + + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: The sum of damage events from pEntity +//----------------------------------------------------------------------------- +int CAchievementData::GetAmountForDamagerInEventHistory( CBaseEntity *pEntity, float flTimeWindow ) +{ + int nAmount = 0; + + for ( int i = 0; i < aDamageEvents.Count(); i++ ) + { + if ( aDamageEvents[i].hEntity != pEntity ) + continue; + + // Msg( " %s : at %.2f (%.2f ago)\n", ToTFPlayer( aDamageEvents[i].hEntity )->GetPlayerName(), aDamageEvents[i].flTimeDamage, gpGlobals->curtime - aDamageEvents[i].flTimeDamage ); + + // Sorted + if ( ( gpGlobals->curtime - aDamageEvents[i].flTimeDamage ) > flTimeWindow ) + break; + + nAmount += aDamageEvents[i].nDamageAmount; + } + + return nAmount; +} + +//----------------------------------------------------------------------------- +// Purpose: Adds hPlayer to the history of people who pushed this player +//----------------------------------------------------------------------------- +void CAchievementData::AddPusherToHistory( EHANDLE hPlayer ) +{ + if ( !hPlayer ) + return; + + EntityHistory_t newHist; + newHist.hEntity = hPlayer; + newHist.flTimeDamage = gpGlobals->curtime; + aPushers.InsertHistory( newHist ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns whether or not pPlayer pushed the player in the time specified +//----------------------------------------------------------------------------- +bool CAchievementData::IsPusherInHistory( CBaseEntity *pPlayer, float flTimeWindow ) +{ + for ( int i = 0; i < aPushers.Count(); i++ ) + { + if ( ( gpGlobals->curtime - aPushers[i].flTimeDamage ) > flTimeWindow ) + return false; + + if ( aPushers[i].hEntity == pPlayer ) + return true; + } + + return false; +} + +#ifdef STAGING_ONLY +CON_COMMAND_F( item_testitem, "Creates a server-side item of the specified type, and gives it to the player. Does NOT create the item on the Steam backend.", FCVAR_NONE ) +{ + if ( args.ArgC() < 2 ) + { + Msg( "Too few parameters. Usage: item_testitem <item definition>\n"); + return; + } + + CTFPlayer *pPlayer = ToTFPlayer( UTIL_GetCommandClient() ); + if ( !pPlayer ) + return; + + CItemSelectionCriteria criteria; + criteria.SetQuality( AE_USE_SCRIPT_VALUE ); + criteria.BAddCondition( "name", k_EOperator_String_EQ, args[1], true ); + + CBaseEntity *pItem = ItemGeneration()->GenerateRandomItem( &criteria, pPlayer->WorldSpaceCenter(), vec3_angle ); + if ( pItem ) + { + CEconItemView *pScriptItem = static_cast<CBaseCombatWeapon*>(pItem)->GetAttributeContainer()->GetItem(); + + // If we already have an identical item, and it's a weapon, remove the current one, and give us this one. + const char *pszItemName = pItem->GetClassname(); + int iClass = pPlayer->GetPlayerClass()->GetClassIndex(); + const char *pszName = TranslateWeaponEntForClass( pszItemName, iClass ); + CBaseEntity *pExisting = pPlayer->Weapon_OwnsThisType(pszName); + if ( pExisting ) + { + CBaseCombatWeapon *pWpn = dynamic_cast<CBaseCombatWeapon *>(pExisting); + pPlayer->Weapon_Detach( pWpn ); + UTIL_Remove( pExisting ); + } + else if ( pItem->IsWearable() ) + { + // If it's a wearable, remove any wearable in the same slot. + for ( int wbl = 0; wbl < pPlayer->GetNumWearables(); wbl++ ) + { + CEconWearable *pWearable = pPlayer->GetWearable(wbl); + if ( !pWearable ) + continue; + + if ( pWearable->GetAttributeContainer()->GetItem()->GetStaticData()->GetLoadoutSlot(iClass) == pScriptItem->GetStaticData()->GetLoadoutSlot(iClass) ) + { + pPlayer->RemoveWearable( pWearable ); + break; + } + } + } + + // Fake global id + pScriptItem->SetItemID( 1 ); + + DispatchSpawn( pItem ); + + CEconEntity *pNewItem = dynamic_cast<CEconEntity*>( pItem ); + Assert( pNewItem ); + if ( pNewItem ) + { + pNewItem->GiveTo( pPlayer ); + } + +#if defined (_DEBUG) + DebugEconItemView( "Generated", pScriptItem ); +#endif + } + else + { + Warning( "Failed to create an item named '%s'\n",args[1]); + } +} + +#endif // STAGING_ONLY + +//----------------------------------------------------------------------------- +// Purpose: Adds this damager to the history list of people whose sentry damaged player +//----------------------------------------------------------------------------- +void CAchievementData::AddSentryDamager( EHANDLE hDamager, EHANDLE hObject ) +{ + if ( !hDamager ) + return; + + EntityHistory_t newHist; + newHist.hEntity = hDamager; + newHist.hObject = hObject; + newHist.flTimeDamage = gpGlobals->curtime; + aSentryDamagers.InsertHistory( newHist ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns whether or not pDamager has damaged the player in the time specified (by way of sentry gun) +//----------------------------------------------------------------------------- +EntityHistory_t* CAchievementData::IsSentryDamagerInHistory( CBaseEntity *pDamager, float flTimeWindow ) +{ + for ( int i = 0; i < aSentryDamagers.Count(); i++ ) + { + if ( ( gpGlobals->curtime - aSentryDamagers[i].flTimeDamage ) > flTimeWindow ) + return NULL; + + if ( aSentryDamagers[i].hEntity == pDamager ) + { + return &aSentryDamagers[i]; + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Client has sent us some KVs describing an item they want to test. +//----------------------------------------------------------------------------- +void CTFPlayer::ItemTesting_Start( KeyValues *pKV ) +{ + static itemid_t s_iTestIndex = 1; + + // We have to be a listen server, with 1 player on it, and the request must come from the listen client. + if ( this != UTIL_GetListenServerHost() ) + return; + int iPlayers = 0; + for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( pPlayer && !pPlayer->IsFakeClient() ) + { + iPlayers++; + } + } + if ( iPlayers > 1 ) + return; + + // We also need to be on the item testing map. + if ( !Q_stricmp(STRING(gpGlobals->mapname), "item_test.bsp" ) ) + return; + + FOR_EACH_VEC( m_ItemsToTest, i ) + { + m_ItemsToTest[i].pKV->deleteThis(); + } + m_ItemsToTest.Purge(); + + TFGameRules()->SetInItemTestingMode( true ); + + int iClassUsage = pKV->GetInt( "class_usage", 0 ); + + ItemTesting_DeleteItems(); // Remove items before creating new defs. Some def clean-up depends on existing static values. + + for ( int iItemType = 0; iItemType < TI_TYPE_COUNT; iItemType++ ) + { + KeyValues *pItemKV = pKV->FindKey( UTIL_VarArgs("Item%d",iItemType) ); + if ( !pItemKV ) + continue; + + // We need to copy these, because the econ item def will want to point at pieces of it + int iNewItem = m_ItemsToTest.AddToTail(); + m_ItemsToTest[iNewItem].pKV = pItemKV->MakeCopy(); + m_ItemsToTest[iNewItem].pKV->SetInt( "class_usage", iClassUsage ); + + bool bTestingExistingItem = pItemKV->GetBool( "test_existing_item", false ); + item_definition_index_t iReplacedItemDef = pItemKV->GetInt( "item_replace", INVALID_ITEM_DEF_INDEX ); + + item_definition_index_t iNewDef = pItemKV->GetInt( "item_def", INVALID_ITEM_DEF_INDEX ); + if ( iNewDef == INVALID_ITEM_DEF_INDEX ) + return; + + // Create the econ item data from it + ItemSystem()->GetItemSchema()->ItemTesting_CreateTestDefinition( iReplacedItemDef, iNewDef, m_ItemsToTest[iNewItem].pKV ); + + // Build our test script item + m_ItemsToTest[iNewItem].scriptItem.Init( iNewDef, AE_USE_SCRIPT_VALUE, AE_USE_SCRIPT_VALUE, false ); + if ( !m_ItemsToTest[iNewItem].scriptItem.GetStaticData() ) + return; + + m_ItemsToTest[iNewItem].scriptItem.SetItemID( s_iTestIndex ); + s_iTestIndex++; + + bool bPrecache = !bTestingExistingItem; + if ( bPrecache ) + { + // Only dynamically load definitions tagged as streamable + GameItemDefinition_t *pEconItemDef = m_ItemsToTest[iNewItem].scriptItem.GetStaticData(); + bPrecache = !pEconItemDef->IsContentStreamable(); + } + if ( bPrecache ) + { + bool bAllowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + for ( int i = 0; i < LOADOUT_COUNT; i++ ) + { + const char *pszModel = m_ItemsToTest[iNewItem].scriptItem.GetStaticData()->GetPlayerDisplayModel(i); + if ( pszModel && pszModel[0] ) + { + int iModelIndex = CBaseEntity::PrecacheModel( pszModel ); + PrecacheGibsForModel( iModelIndex ); + } + } + CBaseEntity::SetAllowPrecache( bAllowPrecache ); + } + } + + // Spawn the right bots, and give them the item + ItemTesting_UpdateBots( pKV ); + + // Make the player respawn (he might have been holding test weapons) + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( 1 ) ); + if ( pPlayer && !pPlayer->IsFakeClient() ) + { + if ( pPlayer->IsAlive() ) + { + pPlayer->m_bItemTestingRespawn = true; + } + pPlayer->ForceRespawn(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ItemTesting_DeleteItems() +{ + // Take away every test item. + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer ) + { + pPlayer->RemoveAllItems(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ItemTesting_UpdateBots( KeyValues *pKV ) +{ + bool bNeedsBot[TF_LAST_NORMAL_CLASS]; + memset( bNeedsBot, 0, sizeof(bNeedsBot) ); + + // Figure out what classes we'll need for all the items we're testing + FOR_EACH_VEC( m_ItemsToTest, i ) + { + CEconItemView *pItem = &m_ItemsToTest[i].scriptItem; + for ( int iClass = TF_FIRST_NORMAL_CLASS; iClass < TF_LAST_NORMAL_CLASS; iClass++ ) + { + if ( pItem->GetStaticData()->CanBeUsedByClass(iClass) ) + { + bNeedsBot[iClass] = true; + } + } + } + + bool bAutoAdd = pKV->GetInt( "auto_add_bots", 1 ) != 0; + bool bBlueTeam = pKV->GetInt( "bots_on_blue_team", 0 ) != 0; + + // Kick every bot that's not one of the valid classes for the item + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsFakeClient() ) + { + int iClass = pPlayer->GetPlayerClass()->GetClassIndex(); + bool bWrongTeam = pPlayer->GetTeamNumber() != (bBlueTeam ? TF_TEAM_BLUE : TF_TEAM_RED); + if ( bAutoAdd && (!bNeedsBot[iClass] || bWrongTeam) ) + { + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", pPlayer->GetUserID() ) ); + } + else + { + bNeedsBot[iClass] = false; + pPlayer->m_bItemTestingRespawn = true; + pPlayer->ForceRespawn(); + } + } + } + + // Spawn bots of each class that uses the item (if we're doing auto addition) + if ( bAutoAdd ) + { + for ( int i = TF_FIRST_NORMAL_CLASS; i < TF_LAST_NORMAL_CLASS; i++ ) + { + if ( bNeedsBot[i] ) + { + engine->ServerCommand( UTIL_VarArgs( "bot -team %s -class %s\n", bBlueTeam ? "blue" : "red", g_aPlayerClassNames_NonLocalized[i] ) ); + } + } + } + + TFGameRules()->ItemTesting_SetupFromKV( pKV ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEconItemView *CTFPlayer::ItemTesting_GetTestItem( int iClass, int iSlot ) +{ + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( 1 ) ); + if ( pPlayer && !pPlayer->IsFakeClient() ) + { + // Loop through all the items we're testing + FOR_EACH_VEC( pPlayer->m_ItemsToTest, i ) + { + CEconItemView *pItem = &pPlayer->m_ItemsToTest[i].scriptItem; + if ( !pItem->GetStaticData()->CanBeUsedByClass( iClass ) ) + continue; + + if ( pItem->GetStaticData()->GetLoadoutSlot( iClass ) == iSlot ) + return pItem; + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::GetReadyToTauntWithPartner( void ) +{ + m_bIsReadyToHighFive = true; + + /*IGameEvent *pEvent = gameeventmanager->CreateEvent( "player_highfive_start" ); + if ( pEvent ) + { + pEvent->SetInt( "entindex", entindex() ); + + gameeventmanager->FireEvent( pEvent ); + }*/ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::CancelTauntWithPartner( void ) +{ + m_bIsReadyToHighFive = false; + + /*IGameEvent *pEvent = gameeventmanager->CreateEvent( "player_highfive_cancel" ); + if ( pEvent ) + { + pEvent->SetInt( "entindex", entindex() ); + + gameeventmanager->FireEvent( pEvent ); + }*/ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::StopTauntSoundLoop() +{ + if ( !m_strTauntSoundLoopName.IsEmpty() ) + { + CReliableBroadcastRecipientFilter filter; + UserMessageBegin( filter, "PlayerTauntSoundLoopEnd" ); + WRITE_BYTE( entindex() ); + MessageEnd(); + + m_strTauntSoundLoopName = ""; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Look for a nearby players who has started a +// high five and is waiting for a partner +//----------------------------------------------------------------------------- +CTFPlayer *CTFPlayer::FindPartnerTauntInitiator( void ) +{ + if ( tf_highfive_debug.GetBool() ) + Msg( "%s looking for a partner taunt initiator.\n", GetPlayerName() ); + + CTFPlayer *pTargetInitiator = NULL; + float flDistSqrToTargetInitiator = FLT_MAX; + + CUtlVector< CTFPlayer* > playerList; + CollectPlayers( &playerList, GetAllowedTauntPartnerTeam(), true ); + for( int t=0; t<playerList.Count(); ++t ) + { + CTFPlayer *pPlayer = playerList[t]; + + if ( pPlayer == this ) + continue; + + // don't allow bot to taunt with each other + if ( pPlayer->IsBot() && IsBot() ) + continue; + + if ( !pPlayer->IsReadyToTauntWithPartner() ) + continue; + + if ( tf_highfive_debug.GetBool() ) + Msg( "%s is ready to %s.\n", pPlayer->GetPlayerName(), pPlayer->m_TauntEconItemView.GetStaticData()->GetDefinitionName() ); + + Vector toPartner = pPlayer->GetAbsOrigin() - GetAbsOrigin(); + float flDistSqrToPlayer = toPartner.LengthSqr(); + if ( flDistSqrToPlayer > Square( tf_highfive_max_range.GetFloat() ) ) + { + if ( tf_highfive_debug.GetBool() ) + Msg( " - but that player was too far away.\n" ); + + // too far away + continue; + } + + // skip if this player is too far to be our initiator + if ( flDistSqrToPlayer >= flDistSqrToTargetInitiator ) + { + if ( tf_highfive_debug.GetBool() ) + { + Msg( " - is further than the current potential initiator.\n" ); + } + continue; + } + + toPartner.NormalizeInPlace(); + + Vector forward; + EyeVectors( &forward ); + + // check if I'm facing this player + if ( DotProduct( toPartner, forward ) < 0.6f ) + { + if ( tf_highfive_debug.GetBool() ) + Msg( " - but we are not looking at that player.\n" ); + + // we are not looking at this partner + continue; + } + + bool bShouldCheckFacing = !pPlayer->m_bTauntMimic; + // check if the player is facing us + if ( bShouldCheckFacing ) + { + Vector partnerForward = pPlayer->BodyDirection2D(); + float toPartnerDotProduct = DotProduct( toPartner, partnerForward ); + if ( tf_highfive_debug.GetBool() ) + Msg( " - dot product to partner is %f\n", toPartnerDotProduct ); + + if ( toPartnerDotProduct > -0.6f ) + { + if ( tf_highfive_debug.GetBool() ) + Msg( " - but that player is not facing us.\n" ); + + // they are not facing us + continue; + } + } + + // check if there's something between us + trace_t result; + CTraceFilterIgnoreTeammates filter( this, COLLISION_GROUP_NONE, GetAllowedTauntPartnerTeam() ); + UTIL_TraceHull( GetAbsOrigin(), pPlayer->GetAbsOrigin(), VEC_HULL_MIN, VEC_HULL_MAX, MASK_PLAYERSOLID, &filter, &result ); + if ( result.DidHit() ) + { + if ( tf_highfive_debug.GetBool() ) + Msg( " - entity [%i %s %s] in between. tracing again with tolerance.\n", + result.GetEntityIndex(), + result.m_pEnt ? result.m_pEnt->GetClassname() : "NULL", + result.surface.name ); + + Vector offset( 0, 0, tf_highfive_height_tolerance.GetFloat() ); + trace_t result2; + UTIL_TraceHull( GetAbsOrigin() + offset, pPlayer->GetAbsOrigin() + offset, VEC_HULL_MIN, VEC_HULL_MAX, MASK_PLAYERSOLID, &filter, &result2 ); + if ( result2.DidHit() ) + { + if ( tf_highfive_debug.GetBool() ) + Msg( " - entity [%i %s %s] in between.\n", + result2.GetEntityIndex(), + result2.m_pEnt ? result2.m_pEnt->GetClassname() : "NULL", + result2.surface.name ); + + // something is in between us + continue; + } + } + + // Check to see if there's a spawn room visualizer between us and our partner + if( PointsCrossRespawnRoomVisualizer( WorldSpaceCenter(), pPlayer->WorldSpaceCenter() ) ) + { + if ( tf_highfive_debug.GetBool() ) + Msg( " - spawn room visualizer in between.\n" ); + + continue; + } + + // update to closer target player + if ( flDistSqrToPlayer < flDistSqrToTargetInitiator ) + { + // success! + if ( tf_highfive_debug.GetBool() ) + Msg( " - is potentially the closest target player.\n" ); + flDistSqrToTargetInitiator = flDistSqrToPlayer; + pTargetInitiator = pPlayer; + } + else if ( tf_highfive_debug.GetBool() ) + { + Msg( " - is further than the current target player.\n" ); + } + } + + // pick the closest target player over the closest player + return pTargetInitiator; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static bool SelectPartnerTaunt( const GameItemDefinition_t *pItemDef, CTFPlayer *initiator, CTFPlayer *receiver, const char **pszInitiatorScene, const char **pszReceiverScene ) +{ + static CSchemaItemDefHandle rpsTaunt( "RPS Taunt" ); + + CTFTauntInfo *pTauntData = pItemDef->GetTauntData(); + if ( !pTauntData ) + return false; + + int iInitiatorClass = initiator->GetPlayerClass()->GetClassIndex(); + int iReceiverClass = receiver->GetPlayerClass()->GetClassIndex(); + + // check if we have any scene + const int iInitiatorSceneCount = pTauntData->GetPartnerTauntInitiatorSceneCount( iInitiatorClass ); + const int iReceiverSceneCount = pTauntData->GetPartnerTauntReceiverSceneCount( iReceiverClass ); + if ( iInitiatorSceneCount == 0 || + iReceiverSceneCount == 0 ) + { + return false; + } + + int iInitiator = 0; + int iReceiver = 0; + if ( pItemDef == rpsTaunt ) + { + Assert( iInitiatorSceneCount == 6 && iReceiverSceneCount == 6 ); + + int iWinner = RandomInt( 0, 2 ); + int iLoser = ( ( iWinner + 2 ) % 3 ) + 3; + + /*static const char* s_pszRPS[3] = { "rock", "paper", "scissor" }; + DevMsg( "%s beats %s\n", s_pszRPS[iWinner], s_pszRPS[iLoser%3] );*/ + + if ( RandomInt( 0, 1 ) ) + { + iInitiator = iWinner; + iReceiver = iLoser; + } + else + { + iInitiator = iLoser; + iReceiver = iWinner; + } + + initiator->SetRPSResult( iInitiator ); + } + else + { + // randomly select a player to pick 0 (could be silent taunt) + // and other player select a different one if there's any + if ( RandomInt( 0, 1 ) == 0 ) + { + iReceiver = iReceiverSceneCount > 1 ? RandomInt( 1, iReceiverSceneCount - 1 ) : 0; + } + else + { + iInitiator = iInitiatorSceneCount > 1 ? RandomInt( 1, iInitiatorSceneCount - 1 ) : 0; + } + } + + *pszInitiatorScene = pTauntData->GetPartnerTauntInitiatorScene( iInitiatorClass, iInitiator ); + *pszReceiverScene = pTauntData->GetPartnerTauntReceiverScene( iReceiverClass, iReceiver ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::AcceptTauntWithPartner( CTFPlayer *initiator ) +{ + if ( !initiator ) + { + return; + } + + if ( tf_highfive_debug.GetBool() ) + Msg( "%s doing %s with initiator %s.\n", GetPlayerName(), initiator->m_TauntEconItemView.GetStaticData()->GetDefinitionName(), initiator->GetPlayerName() ); + + // make sure this won't get us stuck + Vector newOrigin; + float flTolerance; + if ( !initiator->FindOpenTauntPartnerPosition( &initiator->m_TauntEconItemView, newOrigin, &flTolerance )) + { + if ( tf_highfive_debug.GetBool() ) + Msg( " - but there is no open space for us.\n" ); + + return; + } + + trace_t result; + CTraceFilterIgnoreTeammates filter( this, COLLISION_GROUP_NONE, GetTeamNumber() ); + UTIL_TraceHull( newOrigin, newOrigin - Vector( 0, 0, flTolerance ), VEC_HULL_MIN, VEC_HULL_MAX, MASK_PLAYERSOLID, &filter, &result ); + if ( !result.DidHit() ) + { + if ( tf_highfive_debug.GetBool() ) + Msg( " - there's too much space underneath where we need to be.\n" ); + + return; + } + else + { + newOrigin = result.endpos; + } + + trace_t stucktrace; + UTIL_TraceEntity( this, newOrigin, newOrigin, MASK_PLAYERSOLID, &filter, &stucktrace ); + if ( stucktrace.startsolid != 0 ) + { + if ( tf_highfive_debug.GetBool() ) + Msg( " - but we'd get stuck on entity [%i %s %s] going in front of %s.\n", + stucktrace.GetEntityIndex(), + stucktrace.m_pEnt ? stucktrace.m_pEnt->GetClassname() : "NULL", + stucktrace.surface.name, + initiator->GetPlayerName() ); + + return; + } + + // move us into facing position with initiator + SetAbsOrigin( newOrigin ); + QAngle newAngles = initiator->GetAbsAngles(); + // turn 180 degree to face the initiator + newAngles[YAW] = AngleNormalize( newAngles[YAW] - 180 ); + SetAbsAngles( newAngles ); + + m_bIsReadyToHighFive = false; + initiator->m_bIsReadyToHighFive = false; + + // note who our partner is so we can lock our facing toward them on the client + m_hHighFivePartner = initiator; + initiator->m_hHighFivePartner = this; + + if ( initiator->m_hTauntScene.Get() ) + { + StopScriptedScene( initiator, initiator->m_hTauntScene ); + initiator->m_hTauntScene = NULL; + + initiator->StopTauntSoundLoop(); + } + + const char *pszInitiatorScene = NULL; + const char *pszOurScene = NULL; + const GameItemDefinition_t *pItemDef = initiator->m_TauntEconItemView.GetItemDefinition(); + if ( !SelectPartnerTaunt( pItemDef, initiator, this, &pszInitiatorScene, &pszOurScene ) ) + { + if ( tf_highfive_debug.GetBool() ) + { + Msg( "SpeakConceptIfAllowed failed on partner taunt initiator. Aborting taunt.\n" ); + } + AssertMsg( false, "SpeakConceptIfAllowed failed on partner taunt initiator. Aborting taunt." ); + + initiator->m_flTauntRemoveTime = gpGlobals->curtime; + initiator->m_bAllowedToRemoveTaunt = true; + + return; + } + m_TauntEconItemView = initiator->m_TauntEconItemView; + + int initiatorConcept = MP_CONCEPT_HIGHFIVE_SUCCESS_FULL; + int ourConcept = MP_CONCEPT_HIGHFIVE_SUCCESS; + + CMultiplayer_Expresser *pInitiatorExpresser = initiator->GetMultiplayerExpresser(); + Assert( pInitiatorExpresser ); + + pInitiatorExpresser->AllowMultipleScenes(); + + // extend initiator's taunt duration to include actual high five + initiator->m_bInitTaunt = true; + + initiator->PlayScene( pszInitiatorScene ); + + if ( tf_highfive_debug.GetBool() ) + Msg( " concept %i started fine for initiator %s.\n", initiatorConcept, initiator->GetPlayerName() ); + + initiator->m_Shared.m_iTauntIndex = TAUNT_MISC_ITEM; + initiator->m_Shared.m_iTauntConcept.Set( initiatorConcept ); + initiator->m_flTauntRemoveTime = gpGlobals->curtime + GetSceneDuration( pszInitiatorScene ) + 0.2f; + initiator->m_bAllowedToRemoveTaunt = true; + + initiator->m_iTauntAttack = TAUNTATK_NONE; + initiator->m_flTauntAttackTime = 0.f; + + static CSchemaAttributeDefHandle pAttrDef_TauntAttackName( "taunt attack name" ); + const char* pszTauntAttackName = NULL; + if ( FindAttribute_UnsafeBitwiseCast<CAttribute_String>( pItemDef, pAttrDef_TauntAttackName, &pszTauntAttackName ) ) + { + initiator->m_iTauntAttack = GetTauntAttackByName( pszTauntAttackName ); + } + + static CSchemaAttributeDefHandle pAttrDef_TauntAttackTime( "taunt attack time" ); + float flTauntAttackTime = 0.f; + if ( FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pItemDef, pAttrDef_TauntAttackTime, &flTauntAttackTime ) ) + { + initiator->m_flTauntAttackTime = gpGlobals->curtime + flTauntAttackTime; + } + + if ( GetActiveWeapon() ) + { + m_iPreTauntWeaponSlot = GetActiveWeapon()->GetSlot(); + } + + PlayScene( pszOurScene ); + OnTauntSucceeded( pszOurScene, TAUNT_MISC_ITEM, ourConcept ); + + const char *pszTauntSound = pItemDef->GetCustomSound( initiator->GetTeamNumber(), 0 ); + if ( pszTauntSound ) + { + // each participant hears the sound without PAS attenuation, but everyone else gets the PAS attenuation + EmitSound_t params; + params.m_pSoundName = pszTauntSound; + + CSingleUserRecipientFilter soundFilterInitiator( initiator ); + initiator->EmitSound( soundFilterInitiator, initiator->entindex(), params ); + + CSingleUserRecipientFilter soundFilter( this ); + EmitSound( soundFilter, this->entindex(), params ); + + CPASAttenuationFilter attenuationFilter( this, params.m_pSoundName ); + attenuationFilter.RemoveRecipient( this ); + attenuationFilter.RemoveRecipient( initiator ); + initiator->EmitSound( attenuationFilter, initiator->entindex(), params ); + } + + /*static CSchemaItemDefHandle highfiveTaunt( "High Five Taunt" ); + if ( pItemDef == highfiveTaunt ) + { + IGameEvent *pEvent = gameeventmanager->CreateEvent( "player_highfive_success" ); + if ( pEvent ) + { + pEvent->SetInt( "initiator_entindex", initiator->entindex() ); + pEvent->SetInt( "partner_entindex", entindex() ); + + gameeventmanager->FireEvent( pEvent ); + } + }*/ + + initiator->m_bInitTaunt = false; + pInitiatorExpresser->DisallowMultipleScenes(); + + // check for taunt achievements + if ( TFGameRules() && ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP ) ) + { + if ( !IsBot() && !initiator->IsBot() && ( GetTeamNumber() == initiator->GetTeamNumber() ) ) + { + if ( IsCapturingPoint() && initiator->IsCapturingPoint() ) + { + AwardAchievement( ACHIEVEMENT_TF_TAUNT_WHILE_CAPPING ); + initiator->AwardAchievement( ACHIEVEMENT_TF_TAUNT_WHILE_CAPPING ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::MimicTauntFromPartner( CTFPlayer *initiator ) +{ + Assert( initiator->m_bAllowMoveDuringTaunt ); + if ( initiator->m_TauntEconItemView.IsValid() && initiator->m_TauntEconItemView.GetItemDefinition() != NULL ) + { + PlayTauntSceneFromItem( &initiator->m_TauntEconItemView ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +extern ConVar tf_allow_all_team_partner_taunt; +int CTFPlayer::GetAllowedTauntPartnerTeam() const +{ + return tf_allow_all_team_partner_taunt.GetBool() ? TEAM_ANY : GetTeamNumber(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::IncrementKillCountSinceLastDeploy( const CTakeDamageInfo &info ) +{ + // track kills since last deploy, but only if our deployed weapon is the one we + // just killed someone with (this fixes the problem where you fire a rocket, switch + // weapons, and then get the kill tracked on the newly-deployed weapon) + CTFWeaponBase *pTFWeapon = dynamic_cast<CTFWeaponBase *>( info.GetWeapon() ); + if ( pTFWeapon && ( pTFWeapon == GetActiveTFWeapon() ) ) + { + m_Shared.m_iKillCountSinceLastDeploy++; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Return true if any enemy sentry has LOS and is facing me and is in range to attack +//----------------------------------------------------------------------------- +bool CTFPlayer::IsAnyEnemySentryAbleToAttackMe( void ) const +{ + if ( m_Shared.InCond( TF_COND_DISGUISED ) || + m_Shared.InCond( TF_COND_DISGUISING ) || + m_Shared.IsStealthed() ) + { + // I'm a disguised or cloaked Spy + return false; + } + + for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i ) + { + CBaseObject* pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] ); + if ( pObj->ObjectType() != OBJ_SENTRYGUN ) + continue; + + if ( pObj->HasSapper() ) + continue; + + if ( pObj->IsPlasmaDisabled() ) + continue; + + if ( pObj->IsDisabled() ) + continue; + + if ( pObj->IsBuilding() ) + continue; + + if ( pObj->IsCarried() ) + continue; + + // are we in range? + if ( ( GetAbsOrigin() - pObj->GetAbsOrigin() ).IsLengthGreaterThan( SENTRY_MAX_RANGE ) ) + continue; + + // is the sentry aiming towards me? + if ( !IsThreatAimingTowardMe( pObj, 0.95f ) ) + continue; + + // does the sentry have clear line of fire? + if ( !IsLineOfSightClear( pObj, IGNORE_ACTORS ) ) + continue; + + // this sentry can attack me + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// MVM Con Commands +//----------------------------------------------------------------------------- +CON_COMMAND_F( currency_give, "Have some in-game money.", FCVAR_CHEAT ) +{ + CTFPlayer *pPlayer = ToTFPlayer( UTIL_GetCommandClient() ); + if ( !pPlayer ) + return; + + int nAmount = atoi( args[1] ); + +#ifdef STAGING_ONLY + if ( TFGameRules() && TFGameRules()->GameModeUsesExperience() ) + { + // This will give money, and calculate their level + pPlayer->AddExperiencePoints( nAmount, true ); + return; + } +#endif // STAGING_ONLY + + pPlayer->AddCurrency( nAmount ); +} + +//----------------------------------------------------------------------------- +// Purpose: Currency awarded directly will not be tracked by stats - see TFGameRules +//----------------------------------------------------------------------------- +void CTFPlayer::AddCurrency( int nAmount ) +{ + if ( nAmount + m_nCurrency > 30000 ) + { + m_nCurrency = 30000; + } + else if ( nAmount + m_nCurrency < 0 ) + { + m_nCurrency = 0; + } + else + { + m_nCurrency += nAmount; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Remove Currency from Display and track it as currency spent +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveCurrency( int nAmount ) +{ + m_nCurrency = Max( m_nCurrency - nAmount, 0 ); + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + g_pPopulationManager->AddPlayerCurrencySpent( this, nAmount ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Ultra crude experience and level system +//----------------------------------------------------------------------------- +void CTFPlayer::AddExperiencePoints( int nAmount, bool bGiveCurrency /*= false*/, CTFPlayer *pSource /*= NULL*/ ) +{ + int nMyLevel = GetExperienceLevel(); + + // Adjust experience based on level difference of source player + if ( pSource ) + { + int nLevelDiff = pSource->GetExperienceLevel() - nMyLevel; + if ( nLevelDiff <= -5 ) + return; + + if ( nLevelDiff > 0 ) + { + nAmount *= ( nLevelDiff + 1 ); + } + else if ( nLevelDiff < 0 ) + { + nAmount /= ( abs( nLevelDiff ) + 1 ); + } + } + + m_nExperiencePoints += nAmount; + CalculateExperienceLevel(); + + // Money? + if ( bGiveCurrency && TFGameRules() ) + { + TFGameRules()->DistributeCurrencyAmount( nAmount, this, false ); + CTF_GameStats.Event_PlayerCollectedCurrency( this, nAmount ); + EmitSound( "MVM.MoneyPickup" ); + } + + // DevMsg( "Exp: %d, Level: %d Perc: %d\n", GetExperiencePoints(), GetExperienceLevel(), m_nExperienceLevelProgress ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::RefundExperiencePoints( void ) +{ + SetExperienceLevel( 1 ); + + int nAmount = 0; + PlayerStats_t *pPlayerStats = CTF_GameStats.FindPlayerStats( this ); + if ( pPlayerStats ) + { + nAmount = pPlayerStats->statsCurrentRound.m_iStat[TFSTAT_CURRENCY_COLLECTED]; + } + + if ( nAmount > 0 ) + { + SetExperiencePoints(nAmount); + SetCurrency(nAmount); + } + + CalculateExperienceLevel(false); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::CalculateExperienceLevel( bool bAnnounce /*= true*/ ) +{ + int nMyLevel = GetExperienceLevel(); + + int nPrevLevel = nMyLevel; + float flLevel = ( (float)m_nExperiencePoints / 400.f ) + 1.f; + flLevel = Min( flLevel, 20.f ); + + // Ding? + if ( bAnnounce ) + { + if ( flLevel > 1 && nPrevLevel != (int)flLevel ) + { + const char *pszTeamName = GetTeamNumber() == TF_TEAM_RED ? "RED" : "BLU"; + UTIL_ClientPrintAll( HUD_PRINTCENTER, "#TF_PlayerLeveled", pszTeamName, GetPlayerName(), CFmtStr( "%d", (int)flLevel ) ); + UTIL_ClientPrintAll( HUD_PRINTCONSOLE, "#TF_PlayerLeveled", pszTeamName, GetPlayerName(), CFmtStr( "%d", (int)flLevel ) ); + DispatchParticleEffect( "mvm_levelup1", PATTACH_POINT_FOLLOW, this, "head" ); + EmitSound( "Achievement.Earned" ); + } + } + + flLevel = floor( flLevel ); + SetExperienceLevel( Max( flLevel, 1.f ) ); + + // Update level progress percentage - networked + float flLevelPerc = ( flLevel - floor( flLevel ) ) * 100.f; + if ( m_nExperienceLevelProgress != flLevelPerc ) + { + m_nExperienceLevelProgress.Set( (int)flLevelPerc ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Store this upgrade for restoring at a checkpoint +//----------------------------------------------------------------------------- +void CTFPlayer::RememberUpgrade( int iPlayerClass, CEconItemView *pItem, int iUpgrade, int nCost, bool bDowngrade ) +{ + if ( IsBot() ) + return; + + if ( !g_pPopulationManager ) + { + Warning( "Remember Upgrade Error: Population Manager does not exist!\n" ); + return; + } + + if ( TFGameRules() == NULL || !TFGameRules()->GameModeUsesUpgrades() ) + return; + + item_definition_index_t iItemIndex = pItem ? pItem->GetItemDefIndex() : INVALID_ITEM_DEF_INDEX; + + CUtlVector< CUpgradeInfo > *upgrades = g_pPopulationManager->GetPlayerUpgradeHistory( this ); + + if ( !bDowngrade ) + { + CUpgradeInfo info; + info.m_iPlayerClass = iPlayerClass; + info.m_itemDefIndex = iItemIndex; + info.m_upgrade = iUpgrade; + info.m_nCost = nCost; + + if ( upgrades ) + { + upgrades->AddToTail( info ); + } + + m_RefundableUpgrades.AddToTail( info ); + } + else + { + if ( upgrades ) + { + for ( int i = 0; i < upgrades->Count(); ++i ) + { + CUpgradeInfo pInfo = upgrades->Element(i); + if ( ( pInfo.m_itemDefIndex == iItemIndex ) && ( pInfo.m_upgrade == iUpgrade ) && ( pInfo.m_nCost == -nCost ) ) + { + upgrades->Remove( i ); + break; + } + } + } + + // Subset of upgrades that can be sold back + for ( int i = 0; i < m_RefundableUpgrades.Count(); ++i ) + { + CUpgradeInfo pInfo = m_RefundableUpgrades.Element( i ); + if ( ( pInfo.m_itemDefIndex == iItemIndex ) && ( pInfo.m_upgrade == iUpgrade ) && ( pInfo.m_nCost == -nCost ) ) + { + m_RefundableUpgrades.Remove( i ); + break; + } + } + } + + const char *upgradeName = g_hUpgradeEntity->GetUpgradeAttributeName( iUpgrade ); + + DevMsg( "%3.2f: %s: Player '%s', item '%s', upgrade '%s', cost '%d'\n", + gpGlobals->curtime, + bDowngrade ? "FORGET_UPGRADE" : "REMEMBER_UPGRADE", + GetPlayerName(), + pItem ? pItem->GetStaticData()->GetItemBaseName() : "<self>", + upgradeName ? upgradeName : "<NULL>", + nCost ); +} + +//----------------------------------------------------------------------------- +// Purpose: Erase the first upgrade stored for this item (for powerup bottles) +//----------------------------------------------------------------------------- +void CTFPlayer::ForgetFirstUpgradeForItem( CEconItemView *pItem ) +{ + if ( IsBot() ) + return; + + if ( TFGameRules() && !TFGameRules()->IsMannVsMachineMode() ) + return; + + DevMsg( "%3.2f: FORGET_FIRST_UPGRADE_FOR_ITEM: Player '%s', item '%s'\n", + gpGlobals->curtime, + GetPlayerName(), + pItem ? pItem->GetStaticData()->GetItemBaseName() : "<self>" ); + + CUtlVector< CUpgradeInfo > *upgrades = g_pPopulationManager->GetPlayerUpgradeHistory( this ); + if ( upgrades == NULL ) + return; + + for( int i = 0; i < upgrades->Count(); ++i ) + { + if ( ( pItem == NULL && upgrades->Element( i ).m_itemDefIndex == INVALID_ITEM_DEF_INDEX ) || // self upgrade + upgrades->Element(i).m_itemDefIndex == pItem->GetItemDefIndex() ) // item upgrade + { + upgrades->Remove( i ); + g_pPopulationManager->SendUpgradesToPlayer( this ); + break; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ClearUpgradeHistory( void ) +{ + if( !g_pPopulationManager ) + return; + + CUtlVector< CUpgradeInfo > *upgrades = g_pPopulationManager->GetPlayerUpgradeHistory( this ); + if ( upgrades != NULL ) + upgrades->RemoveAll(); + + ResetAccumulatedSentryGunDamageDealt(); + ResetAccumulatedSentryGunKillCount(); + + g_pPopulationManager->SendUpgradesToPlayer( this ); + + DevMsg( "%3.2f: CLEAR_UPGRADE_HISTORY: Player '%s'\n", gpGlobals->curtime, GetPlayerName() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ReapplyItemUpgrades( CEconItemView *pItem ) +{ + if ( IsBot() || !g_pPopulationManager) + return; + + int iClassIndex = GetPlayerClass()->GetClassIndex(); + + // Restore player Upgrades + CUtlVector< CUpgradeInfo > *upgrades = g_pPopulationManager->GetPlayerUpgradeHistory( this ); + if ( upgrades == NULL ) + return; + + BeginPurchasableUpgrades(); + + for( int u = 0; u < upgrades->Count(); ++u ) + { + // Player Upgrades for this class and item + const CUpgradeInfo& upgrade = upgrades->Element(u); + if ( iClassIndex == upgrade.m_iPlayerClass && pItem->GetItemDefIndex() == upgrade.m_itemDefIndex ) + { + g_hUpgradeEntity->ApplyUpgradeToItem( this, pItem, upgrade.m_upgrade, upgrade.m_nCost ); + } + } + + EndPurchasableUpgrades(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ReapplyPlayerUpgrades( void ) +{ + if ( IsBot() || !g_pPopulationManager) + return; + + int iClassIndex = GetPlayerClass()->GetClassIndex(); + RemovePlayerAttributes( false ); + + CUtlVector< CUpgradeInfo > *upgrades = g_pPopulationManager->GetPlayerUpgradeHistory( this ); + if ( upgrades == NULL ) + return; + + BeginPurchasableUpgrades(); + + // Restore player Upgrades + for( int u = 0; u < upgrades->Count(); ++u ) + { + // Player Upgrades for this class + if ( iClassIndex == upgrades->Element(u).m_iPlayerClass) + { + // Upgrades applied to player + if ( upgrades->Element(u).m_itemDefIndex == INVALID_ITEM_DEF_INDEX ) + { + g_hUpgradeEntity->ApplyUpgradeToItem( this, NULL, upgrades->Element(u).m_upgrade, upgrades->Element(u).m_nCost ); + } + } + } + + EndPurchasableUpgrades(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::BeginPurchasableUpgrades( void ) +{ + m_nCanPurchaseUpgradesCount++; + + if ( TFObjectiveResource()->GetMannVsMachineWaveCount() > 1 ) + { + m_RefundableUpgrades.RemoveAll(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::EndPurchasableUpgrades( void ) +{ + AssertMsg( m_nCanPurchaseUpgradesCount > 0, "EndPurchasableUpgrades called when m_nCanPurchaseUpgradesCount <= 0" ); + if ( m_nCanPurchaseUpgradesCount <= 0 ) + return; + + m_nCanPurchaseUpgradesCount--; + + if ( TFObjectiveResource()->GetMannVsMachineWaveCount() > 1 ) + { + m_RefundableUpgrades.RemoveAll(); + } + + // report all upgrades + if ( g_pPopulationManager ) + { + g_pPopulationManager->SendUpgradesToPlayer( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::PlayReadySound( void ) +{ + if ( m_flLastReadySoundTime < gpGlobals->curtime ) + { + if ( TFGameRules() ) + { + int iTeam = GetTeamNumber(); + const char *pszFormat = "%s.Ready"; + + if ( TFGameRules()->IsMannVsMachineMode() ) + { + pszFormat = "%s.ReadyMvM"; + } + else if ( TFGameRules()->IsCompetitiveMode() ) + { + pszFormat = "%s.ReadyComp"; + } + + CFmtStr goYell( pszFormat, g_aPlayerClassNames_NonLocalized[ m_Shared.GetDesiredPlayerClassIndex() ] ); + TFGameRules()->BroadcastSound( iTeam, goYell ); + TFGameRules()->BroadcastSound( TEAM_SPECTATOR, goYell ); // spectators hear the ready sounds, too + + m_flLastReadySoundTime = gpGlobals->curtime + 4.f; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTFPlayer::GetDesiredHeadScale() const +{ + float flDesiredHeadScale = 1.f; + CALL_ATTRIB_HOOK_FLOAT( flDesiredHeadScale, head_scale ); + return flDesiredHeadScale; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTFPlayer::GetHeadScaleSpeed() const +{ + // change size now + if ( + m_Shared.InCond( TF_COND_HALLOWEEN_BOMB_HEAD ) || + m_Shared.InCond( TF_COND_MELEE_ONLY ) || + m_Shared.InCond( TF_COND_HALLOWEEN_KART ) || + m_Shared.InCond( TF_COND_BALLOON_HEAD ) + ) + { + return GetDesiredHeadScale(); + } + + return gpGlobals->frametime; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTFPlayer::GetDesiredTorsoScale() const +{ + float flDesiredTorsoScale = 1.f; + CALL_ATTRIB_HOOK_FLOAT( flDesiredTorsoScale, torso_scale ); + return flDesiredTorsoScale; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTFPlayer::GetTorsoScaleSpeed() const +{ + return gpGlobals->frametime; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTFPlayer::GetDesiredHandScale() const +{ + float flDesiredHandScale = 1.f; + CALL_ATTRIB_HOOK_FLOAT( flDesiredHandScale, hand_scale ); + return flDesiredHandScale; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTFPlayer::GetHandScaleSpeed() const +{ + if ( m_Shared.InCond( TF_COND_MELEE_ONLY ) ) + { + return GetDesiredHandScale(); + } + + return gpGlobals->frametime; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::SetBombHeadTimestamp() +{ + m_fLastBombHeadTimestamp = gpGlobals->curtime; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTFPlayer::GetTimeSinceWasBombHead() const +{ + return gpGlobals->curtime - m_fLastBombHeadTimestamp; +} + +//----------------------------------------------------------------------------- +// Purpose: Can the player breathe under water? +//----------------------------------------------------------------------------- +bool CTFPlayer::CanBreatheUnderwater() const +{ + if ( m_Shared.InCond( TF_COND_SWIMMING_CURSE ) ) + return true; + + int iCanBreatheUnderWater = 0; + CALL_ATTRIB_HOOK_INT( iCanBreatheUnderWater, can_breathe_under_water ); + if ( iCanBreatheUnderWater != 0 ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::InputSpeakResponseConcept( inputdata_t &inputdata ) +{ + const char *pInputString = STRING(inputdata.value.StringID()); + // if no params, early out + if (!pInputString || *pInputString == 0) + { + Warning( "empty SpeakResponse input from %s to %s\n", inputdata.pCaller->GetDebugName(), GetDebugName() ); + return; + } + + char buf[512] = {0}; // temporary for tokenizing + char outputmodifiers[512] = {0}; // eventual output to speak + int outWritten = 0; + V_strncpy(buf, pInputString, 510); + buf[511] = 0; // just in case the last character is a comma -- enforce that the + // last character in the buffer is always a terminator. + // special syntax allowing designers to submit inputs with contexts like + // "concept,context1:value1,context2:value2,context3:value3" + // except that entity i/o seems to eat commas these days (didn't used to be the case) + // so instead of commas we have to use spaces in the entity IO, + // and turn them into commas here. AWESOME. + char *pModifiers = const_cast<char *>(V_strnchr(buf, ' ', 510)); + if ( pModifiers ) + { + *pModifiers = 0; + ++pModifiers; + + // tokenize on spaces + char *token = strtok(pModifiers, " "); + while (token) + { + // find the start characters for the key and value + // (seperated by a : which we replace with null) + char * RESTRICT key = token; + char * RESTRICT colon = const_cast<char *>(V_strnchr(key, ':', 510)); + char * RESTRICT value; + if (!colon) + { + Warning( "faulty context k:v pair in entity io %s\n", pInputString ); + break; + } + + // write the key and colon to the output string + int toWrite = colon - key + 1; + if ( outWritten + toWrite >= 512 ) + { + Warning( "Speak input to %s had overlong parameter %s", GetDebugName(), pInputString ); + return; + } + memcpy(outputmodifiers + outWritten, key, toWrite); + outWritten += toWrite; + + *colon = 0; + value = colon + 1; + + // determine if the value is actually a procedural name + CBaseEntity *pProcedural = gEntList.FindEntityProcedural( value, this, inputdata.pActivator, inputdata.pCaller ); + + // write the value to the output -- if it's a procedural name, replace appropriately; + // if not, just copy over. + const char *valString; + if (pProcedural) + { + valString = STRING(pProcedural->GetEntityName()); + } + else + { + valString = value; + } + toWrite = strlen(valString); + toWrite = MIN( 511-outWritten, toWrite ); + V_strncpy( outputmodifiers + outWritten, valString, toWrite+1 ); + outWritten += toWrite; + + // get the next token + token = strtok(NULL, " "); + if (token) + { + // if there is a next token, write in a comma + if (outWritten < 511) + { + outputmodifiers[outWritten++]=','; + } + } + } + } + + // null terminate just in case + outputmodifiers[outWritten <= 511 ? outWritten : 511] = 0; + + SpeakConceptIfAllowed( GetMPConceptIndexFromString( buf ), outputmodifiers[0] ? outputmodifiers : NULL ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::CreateDisguiseWeaponList( CTFPlayer *pDisguiseTarget ) +{ + ClearDisguiseWeaponList(); + + // copy disguise target weapons + if ( pDisguiseTarget ) + { + for ( int i=0; i<TF_PLAYER_WEAPON_COUNT; ++i ) + { + CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>( pDisguiseTarget->GetWeapon( i ) ); + + if ( !pWeapon ) + continue; + + CEconItemView *pItem = NULL; + // We are copying a generated, non-base item. + CAttributeContainer *pContainer = pWeapon->GetAttributeContainer(); + if ( pContainer ) + { + pItem = pContainer->GetItem(); + } + + int iSubType = 0; + CTFWeaponBase *pCopyWeapon = dynamic_cast<CTFWeaponBase*>( GiveNamedItem( pWeapon->GetClassname(), iSubType, pItem, true ) ); + if ( pCopyWeapon ) + { + pCopyWeapon->AddEffects( EF_NODRAW | EF_NOSHADOW ); + m_hDisguiseWeaponList.AddToTail( pCopyWeapon ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::ClearDisguiseWeaponList() +{ + FOR_EACH_VEC( m_hDisguiseWeaponList, i ) + { + if ( m_hDisguiseWeaponList[i] ) + { + m_hDisguiseWeaponList[i]->Drop( vec3_origin ); + } + } + + m_hDisguiseWeaponList.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::CanScorePointForPD( void ) const +{ + // These conditions block being able to score in PD + ETFCond blockingConds[] = { TF_COND_STEALTHED // Invis spies + , TF_COND_STEALTHED_BLINK + , TF_COND_DISGUISING // Disguised spies + , TF_COND_DISGUISED + , TF_COND_INVULNERABLE // Uber + , TF_COND_PHASE }; // Bonked Scouts + + // Check for blocking conditions + for( int i=0; i<ARRAYSIZE(blockingConds); ++i ) + { + if ( m_Shared.InCond( blockingConds[i] ) ) + { + return false; + } + } + + // More aggressively deny invis than the code above + if ( m_Shared.GetPercentInvisible() > 0.f ) + { + return false; + } + + // Rate limit + return ( ( m_flNextScorePointForPD < 0 ) || ( m_flNextScorePointForPD < gpGlobals->curtime ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::PickupWeaponFromOther( CTFDroppedWeapon *pDroppedWeapon ) +{ + const CEconItemView *pItem = pDroppedWeapon->GetItem(); + if ( !pItem ) + return false; + + if ( pItem->IsValid() ) + { + int iClass = GetPlayerClass()->GetClassIndex(); + int iItemSlot = pItem->GetStaticData()->GetLoadoutSlot( iClass ); + CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase* >( GetEntityForLoadoutSlot( iItemSlot ) ); + + if ( !pWeapon ) + { + AssertMsg( false, "No weapon to put down when picking up a dropped weapon!" ); + return false; + } + + // we need to force translating the name here. + // GiveNamedItem will not translate if we force creating the item + const char *pTranslatedWeaponName = TranslateWeaponEntForClass( pItem->GetStaticData()->GetItemClass(), iClass ); + CTFWeaponBase *pNewItem = dynamic_cast<CTFWeaponBase*>( GiveNamedItem( pTranslatedWeaponName, 0, pItem, true )); + Assert( pNewItem ); + if ( pNewItem ) + { + CTFWeaponBuilder *pBuilder = dynamic_cast<CTFWeaponBuilder*>( (CBaseEntity*)pNewItem ); + if ( pBuilder ) + { + pBuilder->SetSubType( GetPlayerClass()->GetData()->m_aBuildable[0] ); + } + + // make sure we removed our current weapon + if ( pWeapon ) + { + // drop current weapon + Vector vecPackOrigin; + QAngle vecPackAngles; + CalculateAmmoPackPositionAndAngles( pWeapon, vecPackOrigin, vecPackAngles ); + + bool bShouldThrowHeldWeapon = true; + + // When in the spawn room, you won't throw down your held weapon if you own that weapon. + // This is to prevent folks from standing near a supply closet and spawning their items + // over and over and over. + if ( PointInRespawnRoom( this, WorldSpaceCenter() ) ) + { + CSteamID playerSteamID; + GetSteamID( &playerSteamID ); + uint32 nItemAccountID = pWeapon->GetAttributeContainer()->GetItem()->GetAccountID(); + // Stock weapons have accountID 0 + if ( playerSteamID.GetAccountID() == nItemAccountID || nItemAccountID == 0 ) + { + bShouldThrowHeldWeapon = false; + } + } + + if ( bShouldThrowHeldWeapon ) + { + CTFDroppedWeapon *pNewDroppedWeapon = CTFDroppedWeapon::Create( this, vecPackOrigin, vecPackAngles, pWeapon->GetWorldModel(), pWeapon->GetAttributeContainer()->GetItem() ); + if ( pNewDroppedWeapon ) + { + pNewDroppedWeapon->InitDroppedWeapon( this, pWeapon, true ); + } + } + + Weapon_Detach( pWeapon ); + UTIL_Remove( pWeapon ); + } + + CBaseCombatWeapon *pLastWeapon = GetLastWeapon(); + pNewItem->MarkAttachedEntityAsValidated(); + pNewItem->GiveTo( this ); + Weapon_SetLast( pLastWeapon ); + + pDroppedWeapon->InitPickedUpWeapon( this, pNewItem ); + + // can't use the weapon we just picked up? + if ( !Weapon_CanSwitchTo( pNewItem ) ) + { + // try next best thing we can use + SwitchToNextBestWeapon( pNewItem ); + } + + // delay pickup weapon message + m_flSendPickupWeaponMessageTime = gpGlobals->curtime + 0.1f; + + return true; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::TryToPickupDroppedWeapon() +{ + if ( !CanAttack() ) + return false; + + if ( GetActiveWeapon() && ( GetActiveWeapon()->m_flNextPrimaryAttack > gpGlobals->curtime ) ) + return false; + + CTFDroppedWeapon *pDroppedWeapon = GetDroppedWeaponInRange(); + if ( pDroppedWeapon && !pDroppedWeapon->IsMarkedForDeletion() ) + { + if ( PickupWeaponFromOther( pDroppedWeapon ) ) + { + UTIL_Remove( pDroppedWeapon ); + return true; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::AddCustomAttribute( const char *pszAttributeName, float flVal, float flDuration /*= -1.f*/ ) +{ + float flExpireTime = flDuration > 0 ? gpGlobals->curtime + flDuration : flDuration; + int iIndex = m_mapCustomAttributes.Find( pszAttributeName ); + if ( iIndex == m_mapCustomAttributes.InvalidIndex() ) + { + m_mapCustomAttributes.Insert( pszAttributeName, flExpireTime ); + } + else + { + // stomp the previous expire time for now + m_mapCustomAttributes[iIndex] = flExpireTime; + } + + // just stomp the value + m_Shared.ApplyAttributeToPlayer( pszAttributeName, flVal ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveCustomAttribute( const char *pszAttributeName ) +{ + int iIndex = m_mapCustomAttributes.Find( pszAttributeName ); + if ( iIndex != m_mapCustomAttributes.InvalidIndex() ) + { + m_Shared.RemoveAttributeFromPlayer( pszAttributeName ); + m_mapCustomAttributes.RemoveAt( iIndex ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::UpdateCustomAttributes() +{ + // check if we should remove custom attributes from player + bool bShouldCheckCustomAttributes = m_mapCustomAttributes.Count() > 0; + while ( bShouldCheckCustomAttributes ) + { + bShouldCheckCustomAttributes = false; + FOR_EACH_MAP_FAST( m_mapCustomAttributes, i ) + { + float flExpireTime = m_mapCustomAttributes[i]; + if ( flExpireTime > 0 && gpGlobals->curtime > flExpireTime ) + { + const char *pszAttributeName = m_mapCustomAttributes.Key( i ); + m_Shared.RemoveAttributeFromPlayer( pszAttributeName ); + m_mapCustomAttributes.RemoveAt( i ); + + bShouldCheckCustomAttributes = true; + break; + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFPlayer::RemoveAllCustomAttributes() +{ + FOR_EACH_MAP_FAST( m_mapCustomAttributes, i ) + { + const char *pszAttributeName = m_mapCustomAttributes.Key( i ); + m_Shared.RemoveAttributeFromPlayer( pszAttributeName ); + } + m_mapCustomAttributes.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::ShouldForceTransmitsForTeam( int iTeam ) +{ + return ( ( GetTeamNumber() == TEAM_SPECTATOR ) || + ( ( GetTeamNumber() == iTeam ) && ( m_Shared.InCond( TF_COND_TEAM_GLOWS ) || !IsAlive() ) ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::ShouldGetBonusPointsForExtinguishEvent( int userID ) +{ + int iIndex = m_PlayersExtinguished.Find( userID ); + if ( iIndex != m_PlayersExtinguished.InvalidIndex() ) + { + if ( ( gpGlobals->curtime - m_PlayersExtinguished[iIndex] ) < 20.f ) + return false; + + m_PlayersExtinguished[iIndex] = gpGlobals->curtime; + } + else + { + m_PlayersExtinguished.Insert( userID, gpGlobals->curtime ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTFPlayer::IsTruceValidForEnt( void ) const +{ + if ( PointInRespawnRoom( this, WorldSpaceCenter(), true ) ) + return false; + + return true; +} |