You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
22005 lines
668 KiB
22005 lines
668 KiB
//========= 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;
|
|
}
|