summaryrefslogtreecommitdiff
path: root/game/shared/tf/tf_gamerules.cpp
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/shared/tf/tf_gamerules.cpp
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'game/shared/tf/tf_gamerules.cpp')
-rw-r--r--game/shared/tf/tf_gamerules.cpp21410
1 files changed, 21410 insertions, 0 deletions
diff --git a/game/shared/tf/tf_gamerules.cpp b/game/shared/tf/tf_gamerules.cpp
new file mode 100644
index 0000000..5b92cf1
--- /dev/null
+++ b/game/shared/tf/tf_gamerules.cpp
@@ -0,0 +1,21410 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose: The TF Game rules
+//
+// $NoKeywords: $
+//=============================================================================
+#include "cbase.h"
+#include "tf_gamerules.h"
+#include "tf_classdata.h"
+#include "ammodef.h"
+#include "KeyValues.h"
+#include "tf_weaponbase.h"
+#include "tf_weaponbase_gun.h"
+#include "tier0/icommandline.h"
+#include "convar_serverbounded.h"
+#include "econ_item_system.h"
+#include "tf_weapon_grenadelauncher.h"
+#include "tf_logic_robot_destruction.h"
+#include "tf_logic_player_destruction.h"
+#include "tf_matchmaking_shared.h"
+
+#ifdef CLIENT_DLL
+ #include <game/client/iviewport.h>
+ #include "c_tf_player.h"
+ #include "c_tf_objective_resource.h"
+ #include <filesystem.h>
+ #include "c_tf_team.h"
+ #include "dt_utlvector_recv.h"
+ #include "tf_autorp.h"
+ #include "player_vs_environment/c_tf_upgrades.h"
+ #include "video/ivideoservices.h"
+ #include "tf_gc_client.h"
+ #include "c_tf_playerresource.h"
+#else
+ #include "basemultiplayerplayer.h"
+ #include "voice_gamemgr.h"
+ #include "items.h"
+ #include "team.h"
+ #include "game.h"
+ #include "tf_bot_temp.h"
+ #include "tf_player.h"
+ #include "tf_team.h"
+ #include "player_resource.h"
+ #include "entity_tfstart.h"
+ #include "filesystem.h"
+ #include "minigames/tf_duel.h"
+ #include "tf_obj.h"
+ #include "tf_objective_resource.h"
+ #include "tf_player_resource.h"
+ #include "team_control_point_master.h"
+ #include "team_train_watcher.h"
+ #include "playerclass_info_parse.h"
+ #include "team_control_point_master.h"
+ #include "coordsize.h"
+ #include "entity_healthkit.h"
+ #include "tf_gamestats.h"
+ #include "entity_capture_flag.h"
+ #include "tf_player_resource.h"
+ #include "tf_obj_sentrygun.h"
+ #include "activitylist.h"
+ #include "AI_ResponseSystem.h"
+ #include "hl2orange.spa.h"
+ #include "hltvdirector.h"
+ #include "tf_projectile_arrow.h"
+ #include "func_suggested_build.h"
+ #include "tf_gc_api.h"
+ #include "tf_weaponbase_grenadeproj.h"
+ #include "engine/IEngineSound.h"
+ #include "soundenvelope.h"
+ #include "dt_utlvector_send.h"
+ #include "tf_tactical_mission.h"
+ #include "nav_mesh/tf_nav_area.h"
+ #include "bot/tf_bot.h"
+ #include "bot/tf_bot_manager.h"
+ #include "bot/map_entities/tf_bot_roster.h"
+ #include "econ_gcmessages.h"
+ #include "vgui/ILocalize.h"
+ #include "tier3/tier3.h"
+ #include "tf_ammo_pack.h"
+ #include "tf_gcmessages.h"
+ #include "vote_controller.h"
+ #include "tf_voteissues.h"
+ #include "halloween/headless_hatman.h"
+ #include "halloween/ghost/ghost.h"
+ #include "halloween/eyeball_boss/eyeball_boss.h"
+ #include "halloween/merasmus/merasmus.h"
+ #include "halloween/merasmus/merasmus_dancer.h"
+ #include "tf_extra_map_entity.h"
+ #include "tf_weapon_grenade_pipebomb.h"
+ #include "tf_weapon_flaregun.h"
+ #include "tf_weapon_sniperrifle.h"
+ #include "tf_weapon_knife.h"
+ #include "tf_weapon_jar.h"
+ #include "halloween/tf_weapon_spellbook.h"
+
+ #include "player_vs_environment/tf_population_manager.h"
+ #include "player_vs_environment/monster_resource.h"
+ #include "util_shared.h"
+ #include "gc_clientsystem.h"
+
+ #include "raid/tf_raid_logic.h"
+ #include "player_vs_environment/tf_boss_battle_logic.h"
+ #include "player_vs_environment/tf_mann_vs_machine_logic.h"
+ #include "player_vs_environment/tf_upgrades.h"
+
+ #include "tf_wheel_of_doom.h"
+ #include "tf_halloween_souls_pickup.h"
+ #include "halloween/zombie/zombie.h"
+ #include "teamplay_round_timer.h"
+ #include "halloween/spell/tf_spell_pickup.h"
+ #include "tf_weapon_laser_pointer.h"
+ #include "effect_dispatch_data.h"
+ #include "tf_fx.h"
+ #include "econ_game_account_server.h"
+ #include "tf_gc_server.h"
+ #include "tf_logic_halloween_2014.h"
+ #include "tf_obj_sentrygun.h"
+ #include "entity_halloween_pickup.h"
+ #include "entity_rune.h"
+ #include "func_powerupvolume.h"
+ #include "workshop/maps_workshop.h"
+ #include "tf_passtime_logic.h"
+ #include "cdll_int.h"
+ #include "halloween/halloween_gift_spawn_locations.h"
+ #include "tf_weapon_invis.h"
+ #include "tf_gc_server.h"
+ #include "gcsdk/msgprotobuf.h"
+ #include "tf_party.h"
+ #include "tf_autobalance.h"
+#endif
+
+#include "tf_mann_vs_machine_stats.h"
+#include "tf_upgrades_shared.h"
+
+#include "tf_item_powerup_bottle.h"
+#include "tf_weaponbase_gun.h"
+#include "tf_weaponbase_melee.h"
+#include "tf_wearable_item_demoshield.h"
+#include "tf_weapon_buff_item.h"
+#include "tf_weapon_flamethrower.h"
+#include "tf_weapon_medigun.h"
+
+#include "econ_holidays.h"
+#include "rtime.h"
+#include "tf_revive.h"
+#include "tf_duckleaderboard.h"
+
+#include "passtime_convars.h"
+
+#include "tier3/tier3.h"
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+
+#define ITEM_RESPAWN_TIME 10.0f
+#define MASK_RADIUS_DAMAGE ( MASK_SHOT & ~( CONTENTS_HITBOX ) )
+
+// Halloween 2013 VO defines for plr_hightower_event
+#define HELLTOWER_TIMER_INTERVAL ( 60 + RandomInt( -30, 30 ) )
+#define HELLTOWER_RARE_LINE_CHANCE 0.15 // 15%
+#define HELLTOWER_MISC_CHANCE 0.50 // 50%
+
+static int g_TauntCamRagdollAchievements[] =
+{
+ 0, // TF_CLASS_UNDEFINED
+
+ 0, // TF_CLASS_SCOUT,
+ 0, // TF_CLASS_SNIPER,
+ 0, // TF_CLASS_SOLDIER,
+ 0, // TF_CLASS_DEMOMAN,
+ ACHIEVEMENT_TF_MEDIC_FREEZECAM_RAGDOLL, // TF_CLASS_MEDIC,
+ 0, // TF_CLASS_HEAVYWEAPONS,
+ 0, // TF_CLASS_PYRO,
+ ACHIEVEMENT_TF_SPY_FREEZECAM_FLICK, // TF_CLASS_SPY,
+ 0, // TF_CLASS_ENGINEER,
+
+ 0, // TF_CLASS_CIVILIAN,
+ 0, // TF_CLASS_COUNT_ALL,
+};
+
+static int g_TauntCamAchievements[] =
+{
+ 0, // TF_CLASS_UNDEFINED
+
+ 0, // TF_CLASS_SCOUT,
+ ACHIEVEMENT_TF_SNIPER_FREEZECAM_HAT, // TF_CLASS_SNIPER,
+ ACHIEVEMENT_TF_SOLDIER_FREEZECAM_GIBS, // TF_CLASS_SOLDIER, (extra check to count the number of gibs onscreen)
+ ACHIEVEMENT_TF_DEMOMAN_FREEZECAM_SMILE, // TF_CLASS_DEMOMAN,
+ 0, // TF_CLASS_MEDIC,
+ ACHIEVEMENT_TF_HEAVY_FREEZECAM_TAUNT, // TF_CLASS_HEAVYWEAPONS, (there's an extra check on this one to see if we're also invuln)
+ ACHIEVEMENT_TF_PYRO_FREEZECAM_TAUNTS, // TF_CLASS_PYRO,
+ 0, // TF_CLASS_SPY,
+ ACHIEVEMENT_TF_ENGINEER_FREEZECAM_TAUNT, // TF_CLASS_ENGINEER,
+ 0, // TF_CLASS_CIVILIAN,
+ 0, // TF_CLASS_COUNT_ALL,
+};
+
+// used for classes that have more than one freeze cam achievement (example: Sniper)
+static int g_TauntCamAchievements2[] =
+{
+ 0, // TF_CLASS_UNDEFINED
+
+ 0, // TF_CLASS_SCOUT,
+ ACHIEVEMENT_TF_SNIPER_FREEZECAM_WAVE, // TF_CLASS_SNIPER,
+ ACHIEVEMENT_TF_SOLDIER_FREEZECAM_TAUNT, // TF_CLASS_SOLDIER,
+ ACHIEVEMENT_TF_DEMOMAN_FREEZECAM_RUMP, // TF_CLASS_DEMOMAN,
+ 0, // TF_CLASS_MEDIC,
+ 0, // TF_CLASS_HEAVYWEAPONS,
+ 0, // TF_CLASS_PYRO,
+ 0, // TF_CLASS_SPY,
+ 0, // TF_CLASS_ENGINEER,
+
+ 0, // TF_CLASS_CIVILIAN,
+ 0, // TF_CLASS_COUNT_ALL,
+};
+
+struct MapInfo_t
+{
+ const char *pDiskName;
+ const char *pDisplayName;
+ const char *pGameType;
+};
+
+static MapInfo_t s_ValveMaps[] = {
+ { "ctf_2fort", "2Fort", "#Gametype_CTF" },
+ { "cp_dustbowl", "Dustbowl", "#TF_AttackDefend" },
+ { "cp_granary", "Granary", "#Gametype_CP" },
+ { "cp_well", "Well", "#Gametype_CP" },
+ { "cp_foundry", "Foundry", "#Gametype_CP" },
+ { "cp_gravelpit", "Gravel Pit", "#TF_AttackDefend" },
+ { "tc_hydro", "Hydro", "#TF_TerritoryControl" },
+ { "ctf_well", "Well", "#Gametype_CTF" },
+ { "cp_badlands", "Badlands", "#Gametype_CP" },
+ { "pl_goldrush", "Gold Rush", "#Gametype_Escort" },
+ { "pl_badwater", "Badwater Basin", "#Gametype_Escort" },
+ { "plr_pipeline", "Pipeline", "#Gametype_EscortRace" },
+ { "cp_gorge", "Gorge", "#TF_AttackDefend" },
+ { "ctf_doublecross", "Double Cross", "#Gametype_CTF" },
+ { "pl_thundermountain", "Thunder Mountain", "#Gametype_Escort" },
+ { "tr_target", "Target", "#GameType_Training" },
+ { "tr_dustbowl", "Dustbowl", "#GameType_Training" },
+ { "cp_manor_event", "Mann Manor", "#TF_AttackDefend" },
+ { "cp_mountainlab", "Mountain Lab", "#TF_AttackDefend" },
+ { "cp_degrootkeep", "DeGroot Keep", "#TF_MedievalAttackDefend" },
+ { "pl_barnblitz", "Barnblitz", "#Gametype_Escort" },
+ { "pl_upward", "Upward", "#Gametype_Escort" },
+ { "plr_hightower", "Hightower", "#Gametype_EscortRace" },
+ { "koth_viaduct", "Viaduct", "#Gametype_Koth" },
+ { "koth_viaduct_event", "Eyeaduct", "#Gametype_Koth" },
+ { "koth_king", "Kong King", "#Gametype_Koth" },
+ { "koth_lakeside_event", "Ghost Fort", "#Gametype_Koth" },
+ { "plr_hightower_event", "Helltower", "#Gametype_EscortRace" },
+ { "rd_asteroid", "Asteroid", "#Gametype_RobotDestruction" },
+ { "pl_cactuscanyon", "Cactus Canyon", "#Gametype_Escort" },
+ { "sd_doomsday", "Doomsday", "#Gametype_SD" },
+ { "sd_doomsday_event", "Carnival of Carnage", "#Gametype_SD" },
+};
+
+static MapInfo_t s_CommunityMaps[] = {
+ { "pl_borneo", "Borneo", "#Gametype_Escort" },
+ { "koth_suijin", "Suijin", "#Gametype_Koth" },
+ { "cp_snowplow", "Snowplow", "#TF_AttackDefend" },
+ { "koth_probed", "Probed", "#Gametype_Koth" },
+ { "pd_watergate", "Watergate", "#Gametype_PlayerDestruction" },
+ { "arena_byre", "Byre", "#Gametype_Arena" },
+ { "ctf_2fort_invasion", "2Fort Invasion", "#Gametype_CTF" },
+ { "cp_sunshine_event", "Sinshine", "#Gametype_CP" },
+ { "pl_millstone_event", "Hellstone", "#Gametype_Escort" },
+ { "cp_gorge_event", "Gorge Event", "#TF_AttackDefend" },
+ { "koth_moonshine_event", "Moonshine Event", "#Gametype_Koth" },
+ { "pl_snowycoast", "Snowycoast", "#Gametype_Escort" },
+ { "cp_vanguard", "Vanguard", "#Gametype_CP" },
+ { "ctf_landfall", "Landfall", "#Gametype_CTF" },
+ { "koth_highpass", "Highpass", "#Gametype_Koth" },
+ { "koth_maple_ridge_event", "Maple Ridge Event", "#Gametype_Koth" },
+ { "pl_fifthcurve_event", "Brimstone", "#Gametype_Escort" },
+ { "pd_pit_of_death_event", "Pit of Death", "#Gametype_PlayerDestruction" },
+};
+
+/*
+
+ !! Commented out until we use this data, but we should keep updating it so its current when we need it
+
+struct FeaturedWorkshopMap_t
+{
+ // The name the map was shipped as in our files (e.g. it was available as maps/<this>.bsp)
+ // NOTE After maps are un-shipped we leave them here and don't change this value, this allows mapcycles that have
+ // this map to proper redirect to the workshop in the future
+ const char *pShippedName;
+ // The workshop ID of the map (The public one, not the the private upload they use to send us the sources)
+ PublishedFileId_t nWorkshopID;
+};
+
+static FeaturedWorkshopMap_t s_FeaturedWorkshopMaps[] = {
+ // !! DO NOT remove these after we stop shipping the file -- they exist to ensure users can refer to
+ // !! e.g. "koth_suijin" and get redirected to the workshop map once the file is no longer in our depots.
+
+ // Summer 2015 Operation
+ { "pl_borneo", 454139147 },
+ { "koth_suijin", 454188876 },
+ { "cp_snowplow", 454116615 },
+
+ // September 2015 Invasion Community Update
+ { "koth_probed", 454139808 },
+ { "pd_watergate", 456016898 },
+ { "arena_byre", 454142123 },
+ { "ctf_2fort_invasion", FIXME }, // No public workshop entry yet
+
+ // Halloween 2015
+ { "cp_sunshine_event", 532473747 },
+ { "pl_millstone_event", 531384846 },
+ { "cp_gorge_event", 527145539 },
+ { "koth_moonshine_event", 534874830 },
+
+ // December 2015 Campaign
+ { "pl_snowycoast", 469072819 },
+ { "cp_vanguard", 462908782 },
+ { "ctf_landfall", 459651881 },
+ { "koth_highpass", 463803443 },
+
+ // Halloween 2016
+ { "koth_maple_ridge_event", 537540619 },
+ { "pl_fifthcurve_event", 764966851 },
+ { "pd_pit_of_death_event", 537319626 },
+};
+
+*/
+
+bool IsValveMap( const char *pMapName )
+{
+ for ( int i = 0; i < ARRAYSIZE( s_ValveMaps ); ++i )
+ {
+ if ( !Q_stricmp( s_ValveMaps[i].pDiskName, pMapName ) )
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+extern ConVar mp_capstyle;
+extern ConVar sv_turbophysics;
+
+extern ConVar tf_vaccinator_small_resist;
+extern ConVar tf_vaccinator_uber_resist;
+
+extern ConVar tf_teleporter_fov_time;
+extern ConVar tf_teleporter_fov_start;
+
+#ifdef GAME_DLL
+extern ConVar mp_holiday_nogifts;
+extern ConVar tf_debug_damage;
+extern ConVar tf_damage_range;
+extern ConVar tf_damage_disablespread;
+extern ConVar tf_populator_damage_multiplier;
+extern ConVar tf_mm_trusted;
+extern ConVar tf_weapon_criticals;
+extern ConVar tf_weapon_criticals_melee;
+extern ConVar mp_idledealmethod;
+extern ConVar mp_idlemaxtime;
+
+extern ConVar tf_mm_strict;
+extern ConVar mp_autoteambalance;
+
+// STAGING_SPY
+ConVar tf_feign_death_activate_damage_scale( "tf_feign_death_activate_damage_scale", "0.25", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
+ConVar tf_feign_death_damage_scale( "tf_feign_death_damage_scale", "0.35", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
+ConVar tf_stealth_damage_reduction( "tf_stealth_damage_reduction", "0.8", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY );
+
+// training
+ConVar training_class( "training_class", "3", FCVAR_REPLICATED, "Class to use in training." );
+ConVar training_can_build_sentry( "training_can_build_sentry", "1", FCVAR_REPLICATED, "Player can build sentry as engineer." );
+ConVar training_can_build_dispenser( "training_can_build_dispenser", "1", FCVAR_REPLICATED, "Player can build dispenser as engineer." );
+ConVar training_can_build_tele_entrance( "training_can_build_tele_entrance", "1", FCVAR_REPLICATED, "Player can build teleporter entrance as engineer." );
+ConVar training_can_build_tele_exit( "training_can_build_tele_exit", "1", FCVAR_REPLICATED, "Player can build teleporter exit as engineer." );
+ConVar training_can_destroy_buildings( "training_can_destroy_buildings", "1", FCVAR_REPLICATED, "Player can destroy buildings as engineer." );
+ConVar training_can_pickup_sentry( "training_can_pickup_sentry", "1", FCVAR_REPLICATED, "Player can pickup sentry gun as engineer." );
+ConVar training_can_pickup_dispenser( "training_can_pickup_dispenser", "1", FCVAR_REPLICATED, "Player can pickup dispenser as engineer." );
+ConVar training_can_pickup_tele_entrance( "training_can_pickup_tele_entrance", "1", FCVAR_REPLICATED, "Player can pickup teleporter entrance as engineer." );
+ConVar training_can_pickup_tele_exit( "training_can_pickup_tele_exit", "1", FCVAR_REPLICATED, "Player can pickup teleporter entrance as engineer." );
+ConVar training_can_select_weapon_primary ( "training_can_select_weapon_primary", "1", FCVAR_REPLICATED, "In training player select primary weapon." );
+ConVar training_can_select_weapon_secondary ( "training_can_select_weapon_secondary", "1", FCVAR_REPLICATED, "In training player select secondary weapon." );
+ConVar training_can_select_weapon_melee ( "training_can_select_weapon_melee", "1", FCVAR_REPLICATED, "In training player select melee weapon." );
+ConVar training_can_select_weapon_building ( "training_can_select_weapon_building", "1", FCVAR_REPLICATED, "In training player select building tool." );
+ConVar training_can_select_weapon_pda ( "training_can_select_weapon_pda", "1", FCVAR_REPLICATED, "In training player select pda." );
+ConVar training_can_select_weapon_item1 ( "training_can_select_weapon_item1", "1", FCVAR_REPLICATED, "In training player select item 1." );
+ConVar training_can_select_weapon_item2 ( "training_can_select_weapon_item2", "1", FCVAR_REPLICATED, "In training player select item 2." );
+
+ConVar tf_birthday_ball_chance( "tf_birthday_ball_chance", "100", FCVAR_REPLICATED, "Percent chance of a birthday beach ball spawning at each round start" );
+
+ConVar tf_halloween_boss_spawn_interval( "tf_halloween_boss_spawn_interval", "480", FCVAR_CHEAT, "Average interval between boss spawns, in seconds" );
+ConVar tf_halloween_boss_spawn_interval_variation( "tf_halloween_boss_spawn_interval_variation", "60", FCVAR_CHEAT, "Variation of spawn interval +/-" );
+
+ConVar tf_halloween_eyeball_boss_spawn_interval( "tf_halloween_eyeball_boss_spawn_interval", "180", FCVAR_CHEAT, "Average interval between boss spawns, in seconds" );
+ConVar tf_halloween_eyeball_boss_spawn_interval_variation( "tf_halloween_eyeball_boss_spawn_interval_variation", "30", FCVAR_CHEAT, "Variation of spawn interval +/-" );
+
+ConVar tf_merasmus_spawn_interval( "tf_merasmus_spawn_interval", "180", FCVAR_CHEAT, "Average interval between boss spawns, in seconds" );
+ConVar tf_merasmus_spawn_interval_variation( "tf_merasmus_spawn_interval_variation", "30", FCVAR_CHEAT, "Variation of spawn interval +/-" );
+
+ConVar tf_halloween_zombie_mob_enabled( "tf_halloween_zombie_mob_enabled", "0", FCVAR_CHEAT, "If set to 1, spawn zombie mobs on non-Halloween Valve maps" );
+ConVar tf_halloween_zombie_mob_spawn_interval( "tf_halloween_zombie_mob_spawn_interval", "180", FCVAR_CHEAT, "Average interval between zombie mob spawns, in seconds" );
+ConVar tf_halloween_zombie_mob_spawn_count( "tf_halloween_zombie_mob_spawn_count", "20", FCVAR_CHEAT, "How many zombies to spawn" );
+
+ConVar tf_halloween_allow_truce_during_boss_event( "tf_halloween_allow_truce_during_boss_event", "0", FCVAR_NOTIFY, "Determines if RED and BLU can damage each other while fighting Monoculus or Merasmus on non-Valve maps." );
+
+ConVar tf_player_spell_drop_on_death_rate( "tf_player_spell_drop_on_death_rate", "0", FCVAR_REPLICATED );
+ConVar tf_player_drop_bonus_ducks( "tf_player_drop_bonus_ducks", "-1", FCVAR_REPLICATED, "-1 Default (Holiday-based)\n0 - Force off\n1 - Force on" );
+
+ConVar tf_player_name_change_time( "tf_player_name_change_time", "60", FCVAR_NOTIFY, "Seconds between name changes." );
+
+ConVar tf_weapon_criticals_distance_falloff( "tf_weapon_criticals_distance_falloff", "0", FCVAR_CHEAT, "Critical weapon damage will take distance into account." );
+ConVar tf_weapon_minicrits_distance_falloff( "tf_weapon_minicrits_distance_falloff", "0", FCVAR_CHEAT, "Mini-crit weapon damage will take distance into account." );
+
+ConVar mp_spectators_restricted( "mp_spectators_restricted", "0", FCVAR_NONE, "Prevent players on game teams from joining team spectator if it would unbalance the teams." );
+
+ConVar tf_test_special_ducks( "tf_test_special_ducks", "1", FCVAR_DEVELOPMENTONLY );
+
+ConVar tf_mm_abandoned_players_per_team_max( "tf_mm_abandoned_players_per_team_max", "1", FCVAR_DEVELOPMENTONLY );
+
+#endif // GAME_DLL
+ConVar tf_mm_next_map_vote_time( "tf_mm_next_map_vote_time", "30", FCVAR_REPLICATED );
+
+#ifdef STAGING_ONLY
+#ifdef GAME_DLL
+void cc_tf_truce_changed( IConVar *pConVar, const char *pOldString, float flOldValue )
+{
+
+ if ( TFGameRules() )
+ {
+ TFGameRules()->RecalculateTruce();
+ }
+}
+#endif // GAME_DLL
+ConVar tf_truce( "tf_truce", "0", FCVAR_REPLICATED, "Force a team truce on or off.", true, 0, true, 1
+#ifdef GAME_DLL
+ , cc_tf_truce_changed
+#endif // GAME_DLL
+ );
+#endif // STAGING_ONLY
+
+static float g_fEternaweenAutodisableTime = 0.0f;
+
+ConVar tf_spec_xray( "tf_spec_xray", "1", FCVAR_NOTIFY | FCVAR_REPLICATED, "Allows spectators to see player glows. 1 = same team, 2 = both teams" );
+ConVar tf_spawn_glows_duration( "tf_spawn_glows_duration", "10", FCVAR_NOTIFY | FCVAR_REPLICATED, "How long should teammates glow after respawning\n" );
+
+#ifdef GAME_DLL
+void cc_tf_forced_holiday_changed( IConVar *pConVar, const char *pOldString, float flOldValue )
+{
+ // Tell the listeners to recalculate the holiday
+ IGameEvent *event = gameeventmanager->CreateEvent( "recalculate_holidays" );
+ if ( event )
+ {
+ gameeventmanager->FireEvent( event );
+ }
+}
+#endif // GAME_DLL
+
+ConVar tf_forced_holiday( "tf_forced_holiday", "0", FCVAR_REPLICATED, "Forced holiday, \n Birthday = 1\n Halloween = 2\n" // Christmas = 3\n Valentines = 4\n MeetThePyro = 5\n FullMoon=6
+#ifdef GAME_DLL
+, cc_tf_forced_holiday_changed
+#endif // GAME_DLL
+);
+ConVar tf_item_based_forced_holiday( "tf_item_based_forced_holiday", "0", FCVAR_REPLICATED, "" // like a clone of tf_forced_holiday, but controlled by client consumable item use
+#ifdef GAME_DLL
+ , cc_tf_forced_holiday_changed
+#endif // GAME_DLL
+);
+ConVar tf_force_holidays_off( "tf_force_holidays_off", "0", FCVAR_NOTIFY | FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY, ""
+#ifdef GAME_DLL
+, cc_tf_forced_holiday_changed
+#endif // GAME_DLL
+);
+ConVar tf_birthday( "tf_birthday", "0", FCVAR_NOTIFY | FCVAR_REPLICATED );
+ConVar tf_spells_enabled( "tf_spells_enabled", "0", FCVAR_NOTIFY | FCVAR_REPLICATED, "Enable to Allow Halloween Spells to be dropped and used by players" );
+
+ConVar tf_caplinear( "tf_caplinear", "1", FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY, "If set to 1, teams must capture control points linearly." );
+ConVar tf_stalematechangeclasstime( "tf_stalematechangeclasstime", "20", FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY, "Amount of time that players are allowed to change class in stalemates." );
+ConVar mp_tournament_redteamname( "mp_tournament_redteamname", "RED", FCVAR_REPLICATED | FCVAR_HIDDEN );
+ConVar mp_tournament_blueteamname( "mp_tournament_blueteamname", "BLU", FCVAR_REPLICATED | FCVAR_HIDDEN );
+
+//tagES revisit this later
+ConVar tf_attack_defend_map( "tf_attack_defend_map", "0", FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY );
+
+#ifdef GAME_DLL
+void cc_tf_stopwatch_changed( IConVar *pConVar, const char *pOldString, float flOldValue )
+{
+ IGameEvent *event = gameeventmanager->CreateEvent( "stop_watch_changed" );
+ if ( event )
+ {
+ gameeventmanager->FireEvent( event );
+ }
+}
+#endif // GAME_DLL
+ConVar mp_tournament_stopwatch( "mp_tournament_stopwatch", "1", FCVAR_REPLICATED | FCVAR_NOTIFY, "Use Stopwatch mode while using Tournament mode (mp_tournament)"
+#ifdef GAME_DLL
+ , cc_tf_stopwatch_changed
+#endif
+);
+ConVar mp_tournament_readymode( "mp_tournament_readymode", "0", FCVAR_REPLICATED | FCVAR_NOTIFY, "Enable per-player ready status for tournament mode." );
+ConVar mp_tournament_readymode_min( "mp_tournament_readymode_min", "2", FCVAR_REPLICATED | FCVAR_NOTIFY, "Minimum number of players required on the server before players can toggle ready status." );
+ConVar mp_tournament_readymode_team_size( "mp_tournament_readymode_team_size", "0", FCVAR_REPLICATED | FCVAR_NOTIFY, "Minimum number of players required to be ready per-team before the game can begin." );
+ConVar mp_tournament_readymode_countdown( "mp_tournament_readymode_countdown", "10", FCVAR_REPLICATED | FCVAR_NOTIFY, "The number of seconds before a match begins when both teams are ready." );
+ConVar mp_windifference( "mp_windifference", "0", FCVAR_REPLICATED | FCVAR_NOTIFY, "Score difference between teams before server changes maps", true, 0, false, 0 );
+ConVar mp_windifference_min( "mp_windifference_min", "0", FCVAR_REPLICATED | FCVAR_NOTIFY, "Minimum score needed for mp_windifference to be applied", true, 0, false, 0 );
+
+ConVar tf_tournament_classlimit_scout( "tf_tournament_classlimit_scout", "-1", FCVAR_REPLICATED, "Tournament mode per-team class limit for Scouts.\n" );
+ConVar tf_tournament_classlimit_sniper( "tf_tournament_classlimit_sniper", "-1", FCVAR_REPLICATED, "Tournament mode per-team class limit for Snipers.\n" );
+ConVar tf_tournament_classlimit_soldier( "tf_tournament_classlimit_soldier", "-1", FCVAR_REPLICATED, "Tournament mode per-team class limit for Soldiers.\n" );
+ConVar tf_tournament_classlimit_demoman( "tf_tournament_classlimit_demoman", "-1", FCVAR_REPLICATED, "Tournament mode per-team class limit for Demomenz.\n" );
+ConVar tf_tournament_classlimit_medic( "tf_tournament_classlimit_medic", "-1", FCVAR_REPLICATED, "Tournament mode per-team class limit for Medics.\n" );
+ConVar tf_tournament_classlimit_heavy( "tf_tournament_classlimit_heavy", "-1", FCVAR_REPLICATED, "Tournament mode per-team class limit for Heavies.\n" );
+ConVar tf_tournament_classlimit_pyro( "tf_tournament_classlimit_pyro", "-1", FCVAR_REPLICATED, "Tournament mode per-team class limit for Pyros.\n" );
+ConVar tf_tournament_classlimit_spy( "tf_tournament_classlimit_spy", "-1", FCVAR_REPLICATED, "Tournament mode per-team class limit for Spies.\n" );
+ConVar tf_tournament_classlimit_engineer( "tf_tournament_classlimit_engineer", "-1", FCVAR_REPLICATED, "Tournament mode per-team class limit for Engineers.\n" );
+ConVar tf_tournament_classchange_allowed( "tf_tournament_classchange_allowed", "1", FCVAR_REPLICATED, "Allow players to change class while the game is active?.\n" );
+ConVar tf_tournament_classchange_ready_allowed( "tf_tournament_classchange_ready_allowed", "1", FCVAR_REPLICATED, "Allow players to change class after they are READY?.\n" );
+
+ConVar tf_classlimit( "tf_classlimit", "0", FCVAR_REPLICATED | FCVAR_NOTIFY, "Limit on how many players can be any class (i.e. tf_class_limit 2 would limit 2 players per class).\n", true, 0.f, false, 0.f );
+ConVar tf_player_movement_restart_freeze( "tf_player_movement_restart_freeze", "1", FCVAR_REPLICATED, "When set, prevent player movement during round restart" );
+
+ConVar tf_autobalance_query_lifetime( "tf_autobalance_query_lifetime", "30", FCVAR_REPLICATED );
+ConVar tf_autobalance_xp_bonus( "tf_autobalance_xp_bonus", "150", FCVAR_REPLICATED );
+
+//tagES
+#ifdef STAGING_ONLY
+ConVar tf_test_match_summary( "tf_test_match_summary", "0", FCVAR_REPLICATED );
+
+#ifdef GAME_DLL
+void cc_tf_fake_mm_changed( IConVar *pConVar, const char *pOldString, float flOldValue )
+{
+ ConVarRef var( pConVar );
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( (EMatchGroup)var.GetInt() );
+ if ( pMatchDesc )
+ {
+ pMatchDesc->InitServerSettingsForMatch( NULL );
+ }
+}
+#endif // GAME_DLL
+ConVar tf_fake_mm_group( "tf_fake_mm_group", "-1", FCVAR_REPLICATED, "Fake what kind of MM group is being played"
+#ifdef GAME_DLL
+ , cc_tf_fake_mm_changed
+#endif // GAME_DLL
+ );
+#endif // STAGING_ONLY
+
+#ifdef GAME_DLL
+
+static const float g_flStrangeEventBatchProcessInterval = 30.0f;
+
+ConVar mp_humans_must_join_team("mp_humans_must_join_team", "any", FCVAR_REPLICATED, "Restricts human players to a single team {any, blue, red, spectator}" );
+
+void cc_tf_medieval_changed( IConVar *pConVar, const char *pOldString, float flOldValue )
+{
+ ConVarRef var( pConVar );
+ bool bOldValue = flOldValue > 0;
+ if ( var.IsValid() && ( bOldValue != var.GetBool() ) )
+ {
+ Msg( "Medieval mode changes take effect after the next map change.\n" );
+ }
+}
+
+#endif
+ConVar tf_medieval( "tf_medieval", "0", FCVAR_REPLICATED | FCVAR_NOTIFY, "Enable Medieval Mode.\n", true, 0, true, 1
+#ifdef GAME_DLL
+ , cc_tf_medieval_changed
+#endif
+ );
+
+ConVar tf_medieval_autorp( "tf_medieval_autorp", "1", FCVAR_REPLICATED | FCVAR_NOTIFY, "Enable Medieval Mode auto-roleplaying.\n", true, 0, true, 1 );
+
+ConVar tf_sticky_radius_ramp_time( "tf_sticky_radius_ramp_time", "2.0", FCVAR_DEVELOPMENTONLY | FCVAR_CHEAT | FCVAR_REPLICATED, "Amount of time to get full radius after arming" );
+ConVar tf_sticky_airdet_radius( "tf_sticky_airdet_radius", "0.85", FCVAR_DEVELOPMENTONLY | FCVAR_CHEAT | FCVAR_REPLICATED, "Radius Scale if detonated in the air" );
+
+#ifdef STAGING_ONLY
+ConVar tf_killstreak_alwayson( "tf_killstreak_alwayson", "0", FCVAR_REPLICATED, "enable to have killstreak effects for all players, bots included");
+
+#ifdef GAME_DLL
+// Bounty Mode
+ConVar tf_bountymode_currency_starting( "tf_bountymode_currency_starting", "1000", FCVAR_ARCHIVE, "How much new players start with when playing Bounty Mode.\n" );
+ConVar tf_bountymode_currency_limit( "tf_bountymode_currency_limit", "0", FCVAR_ARCHIVE, "The maximum amount a player can hold in Bounty Mode.\n" );
+ConVar tf_bountymode_currency_penalty_ondeath( "tf_bountymode_currency_penalty_ondeath", "0", FCVAR_ARCHIVE, "The percentage of unspent money players lose when they die in Bounty Mode.\n" );
+ConVar tf_bountymode_upgrades_wipeondeath( "tf_bountymode_upgrades_wipeondeath", "0", FCVAR_ARCHIVE, "If set to true, wipe player/item upgrades on death.\n" );
+
+void cc_bountymode_changed( IConVar *pConVar, const char *pOldString, float flOldValue )
+{
+ ConVarRef var( pConVar );
+ if ( var.IsValid() && TFGameRules() )
+ {
+ TFGameRules()->SetBountyMode( var.GetBool() );
+
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ return;
+
+ mp_restartgame_immediate.SetValue( 1 );
+
+ if ( !g_pPopulationManager )
+ {
+ CreateEntityByName( "info_populator" );
+ }
+
+ int nCurrency = tf_bountymode_currency_starting.GetInt();
+ if ( nCurrency > 0 )
+ {
+ // Give everyone starting money
+ for ( int i = 0; i <= MAX_PLAYERS; ++i )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( !pPlayer )
+ continue;
+
+ pPlayer->SetCurrency( nCurrency );
+ }
+ }
+
+ g_MannVsMachineUpgrades.LoadUpgradesFile();
+
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "upgrades_file_changed" );
+ if ( pEvent )
+ {
+ pEvent->SetString( "path", "" ); // Have the client load the default
+ gameeventmanager->FireEvent( pEvent );
+ }
+ }
+}
+#endif // GAME_DLL
+
+ConVar tf_bountymode( "tf_bountymode", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_ARCHIVE, "Allow upgrades and award currency for mission objectives and killing enemy players.\n", true, 0, true, 1
+#ifdef GAME_DLL
+ , cc_bountymode_changed
+#endif // GAME_DLL
+ );
+#endif // STAGING_ONLY
+
+#ifndef GAME_DLL
+extern ConVar cl_burninggibs;
+extern ConVar english;
+ConVar tf_particles_disable_weather( "tf_particles_disable_weather", "0", FCVAR_ARCHIVE, "Disable particles related to weather effects." );
+#endif
+
+// arena mode cvars
+ConVar tf_arena_force_class( "tf_arena_force_class", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY | FCVAR_CHEAT, "Forces players to play a random class each time they spawn." );
+ConVar tf_arena_change_limit( "tf_arena_change_limit", "1", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY | FCVAR_CHEAT, "Number of times players can change their class when mp_force_random_class is being used." );
+ConVar tf_arena_override_cap_enable_time( "tf_arena_override_cap_enable_time", "-1", FCVAR_REPLICATED | FCVAR_NOTIFY, "Overrides the time (in seconds) it takes for the capture point to become enable, -1 uses the level designer specified time." );
+ConVar tf_arena_override_team_size( "tf_arena_override_team_size", "0", FCVAR_REPLICATED, "Overrides the maximum team size in arena mode. Set to zero to keep the default behavior of 1/3 maxplayers.");
+ConVar tf_arena_first_blood( "tf_arena_first_blood", "1", FCVAR_REPLICATED | FCVAR_NOTIFY, "Rewards the first player to get a kill each round." );
+extern ConVar tf_arena_preround_time;
+extern ConVar tf_arena_max_streak;
+#if defined( _DEBUG ) || defined( STAGING_ONLY )
+extern ConVar mp_developer;
+#endif // _DEBUG || STAGING_ONLY
+
+//=============================================================================
+// HPE_BEGIN
+// [msmith] Used for the client to tell the server that we're watching a movie or not.
+// Also contains the name of a movie if it's an in game video.
+//=============================================================================
+// Training mode cvars
+ConVar tf_training_client_message( "tf_training_client_message", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "A simple way for the training client to communicate with the server." );
+//=============================================================================
+// HPE_END
+//=============================================================================
+
+#define TF_ARENA_MODE_FIRST_BLOOD_CRIT_TIME 5.0f
+#define TF_ARENA_MODE_FAST_FIRST_BLOOD_TIME 20.0f
+#define TF_ARENA_MODE_SLOW_FIRST_BLOOD_TIME 50.0f
+
+#ifdef TF_RAID_MODE
+// Raid mode
+ConVar tf_gamemode_raid( "tf_gamemode_raid", "0", FCVAR_REPLICATED | FCVAR_NOTIFY ); // client needs access to this for IsRaidMode()
+ConVar tf_raid_enforce_unique_classes( "tf_raid_enforce_unique_classes", "0", FCVAR_REPLICATED | FCVAR_NOTIFY );
+ConVar tf_raid_respawn_time( "tf_raid_respawn_time", "5", FCVAR_REPLICATED | FCVAR_NOTIFY /*| FCVAR_CHEAT*/, "How long it takes for a Raider to respawn with his team after death." );
+ConVar tf_raid_allow_all_classes( "tf_raid_allow_all_classes", "1", FCVAR_REPLICATED | FCVAR_NOTIFY );
+
+ConVar tf_gamemode_boss_battle( "tf_gamemode_boss_battle", "0", FCVAR_REPLICATED | FCVAR_NOTIFY );
+
+#ifdef GAME_DLL
+ConVar tf_raid_allow_overtime( "tf_raid_allow_overtime", "0"/*, FCVAR_CHEAT*/ );
+#endif // GAME_DLL
+#endif // TF_RAID_MODE
+
+enum { kMVM_MaxConnectedPlayers = 10, };
+
+ConVar tf_mvm_min_players_to_start( "tf_mvm_min_players_to_start", "3", FCVAR_REPLICATED | FCVAR_NOTIFY, "Minimum number of players connected to start a countdown timer" );
+ConVar tf_mvm_respec_enabled( "tf_mvm_respec_enabled", "1", FCVAR_CHEAT | FCVAR_REPLICATED, "Allow players to refund credits spent on player and item upgrades." );
+ConVar tf_mvm_respec_limit( "tf_mvm_respec_limit", "0", FCVAR_CHEAT | FCVAR_REPLICATED, "The total number of respecs a player can earn. Default: 0 (no limit).", true, 0.f, true, 100.f );
+ConVar tf_mvm_respec_credit_goal( "tf_mvm_respec_credit_goal", "2000", FCVAR_CHEAT | FCVAR_REPLICATED, "When tf_mvm_respec_limit is non-zero, the total amount of money the team must collect to earn a respec credit." );
+#ifdef STAGING_ONLY
+ConVar tf_mvm_buybacks_method( "tf_mvm_buybacks_method", "1", FCVAR_REPLICATED | FCVAR_HIDDEN, "When set to 0, use the traditional, currency-based system. When set to 1, use finite, charge-based system.", true, 0.0, true, 1.0 );
+#else
+ConVar tf_mvm_buybacks_method( "tf_mvm_buybacks_method", "0", FCVAR_REPLICATED | FCVAR_HIDDEN, "When set to 0, use the traditional, currency-based system. When set to 1, use finite, charge-based system.", true, 0.0, true, 1.0 );
+#endif
+ConVar tf_mvm_buybacks_per_wave( "tf_mvm_buybacks_per_wave", "3", FCVAR_REPLICATED | FCVAR_HIDDEN, "The fixed number of buybacks players can use per-wave." );
+
+
+#ifdef GAME_DLL
+enum { kMVM_CurrencyPackMinSize = 1, };
+#endif // GAME_DLL
+
+extern ConVar mp_tournament;
+extern ConVar mp_tournament_post_match_period;
+
+extern ConVar tf_flag_return_on_touch;
+extern ConVar tf_flag_return_time_credit_factor;
+ConVar tf_grapplinghook_enable( "tf_grapplinghook_enable", "0", FCVAR_REPLICATED );
+
+#ifdef GAME_DLL
+CUtlString s_strNextMvMPopFile;
+CON_COMMAND_F( tf_mvm_popfile, "Change to a target popfile for MvM", FCVAR_GAMEDLL )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ if ( args.ArgC() <= 1 )
+ {
+ if ( TFGameRules() && g_pPopulationManager )
+ {
+ const char *pszFile = g_pPopulationManager->GetPopulationFilename();
+ if ( pszFile && pszFile[0] )
+ {
+ Msg( "Current popfile is: %s\n", pszFile );
+ return;
+ }
+ }
+
+ Msg( "Missing Popfile name\n" );
+ return;
+ }
+
+ const char *pszShortName = args.Arg(1);
+
+ if ( !TFGameRules() || !g_pPopulationManager )
+ {
+ Warning( "Cannot set population file before map load.\n" );
+ return;
+ }
+
+ // Make sure we have a file system
+ if ( !g_pFullFileSystem )
+ {
+ Msg( "No File System to find Popfile to load\n" );
+ return;
+ }
+
+ // Form full path
+ CUtlString fullPath;
+
+ if ( g_pPopulationManager->FindPopulationFileByShortName( pszShortName, fullPath ) )
+ {
+ g_pPopulationManager->SetPopulationFilename( fullPath );
+ g_pPopulationManager->ResetMap();
+ return;
+ }
+
+ // Give them a message to make it clear what file we were looking for
+ Warning( "Could not find a population file matching: %s.\n", pszShortName );
+}
+
+#ifdef STAGING_ONLY
+// Never ship this
+CON_COMMAND_F( tf_competitive_mode_force_victory, "For testing.", FCVAR_GAMEDLL )
+{
+ // Listenserver host or rcon access only!
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_GetCommandClient() );
+ if ( !pPlayer )
+ return;
+
+ CSteamID steamIDForPlayer;
+ if ( !pPlayer->GetSteamID( &steamIDForPlayer ) )
+ return;
+
+ // TODO: Rewrite this based on the shipping version of comp/casual
+}
+#endif // STAGING_ONLY
+#endif
+
+static bool BIsCvarIndicatingHolidayIsActive( int iCvarValue, /*EHoliday*/ int eHoliday )
+{
+ if ( iCvarValue == 0 )
+ return false;
+
+ // Unfortunately Holidays are not a proper bitfield
+ switch ( eHoliday )
+ {
+ case kHoliday_TFBirthday: return iCvarValue == kHoliday_TFBirthday;
+ case kHoliday_Halloween: return iCvarValue == kHoliday_Halloween || iCvarValue == kHoliday_HalloweenOrFullMoon || iCvarValue == kHoliday_HalloweenOrFullMoonOrValentines;
+ case kHoliday_Christmas: return iCvarValue == kHoliday_Christmas;
+ case kHoliday_Valentines: return iCvarValue == kHoliday_Valentines || iCvarValue == kHoliday_HalloweenOrFullMoonOrValentines;
+ case kHoliday_MeetThePyro: return iCvarValue == kHoliday_MeetThePyro;
+ case kHoliday_FullMoon: return iCvarValue == kHoliday_FullMoon || iCvarValue == kHoliday_HalloweenOrFullMoon || iCvarValue == kHoliday_HalloweenOrFullMoonOrValentines;
+ case kHoliday_HalloweenOrFullMoon: return iCvarValue == kHoliday_Halloween || iCvarValue == kHoliday_FullMoon || iCvarValue == kHoliday_HalloweenOrFullMoon || iCvarValue == kHoliday_HalloweenOrFullMoonOrValentines;
+ case kHoliday_HalloweenOrFullMoonOrValentines: return iCvarValue == kHoliday_Halloween || iCvarValue == kHoliday_FullMoon || iCvarValue == kHoliday_Valentines || iCvarValue == kHoliday_HalloweenOrFullMoon || iCvarValue == kHoliday_HalloweenOrFullMoonOrValentines;
+ case kHoliday_AprilFools: return iCvarValue == kHoliday_AprilFools;
+ case kHoliday_EOTL: return iCvarValue == kHoliday_EOTL;
+ case kHoliday_CommunityUpdate: return iCvarValue == kHoliday_CommunityUpdate;
+ }
+
+ return false;
+}
+
+// Fetch holiday setting taking into account convars, etc, but NOT
+// taking into consideration the current game rules, map, etc.
+//
+// This version can be used outside of gameplay, ie., for matchmaking
+bool TF_IsHolidayActive( /*EHoliday*/ int eHoliday )
+{
+ if ( IsX360() || tf_force_holidays_off.GetBool() )
+ return false;
+
+ if ( BIsCvarIndicatingHolidayIsActive( tf_forced_holiday.GetInt(), eHoliday ) )
+ return true;
+
+ if ( BIsCvarIndicatingHolidayIsActive( tf_item_based_forced_holiday.GetInt(), eHoliday ) )
+ return true;
+
+ if ( ( eHoliday == kHoliday_TFBirthday ) && tf_birthday.GetBool() )
+ return true;
+
+ if ( TFGameRules() )
+ {
+ if ( eHoliday == kHoliday_HalloweenOrFullMoon )
+ {
+ if ( TFGameRules()->IsHolidayMap( kHoliday_Halloween ) )
+ return true;
+ if ( TFGameRules()->IsHolidayMap( kHoliday_FullMoon ) )
+ return true;
+ }
+ if ( TFGameRules()->IsHolidayMap( eHoliday ) )
+ {
+ return true;
+ }
+ }
+
+ return UTIL_IsHolidayActive( eHoliday );
+}
+
+#ifdef TF_CREEP_MODE
+ConVar tf_gamemode_creep_wave( "tf_gamemode_creep_wave", "0", FCVAR_REPLICATED | FCVAR_NOTIFY );
+ConVar tf_creep_wave_player_respawn_time( "tf_creep_wave_player_respawn_time", "10", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_CHEAT, "How long it takes for a player to respawn with his team after death." );
+#endif
+
+#ifdef GAME_DLL
+// TF overrides the default value of this convar
+
+#ifdef _DEBUG
+#define WAITING_FOR_PLAYERS_FLAGS 0
+#else
+#define WAITING_FOR_PLAYERS_FLAGS FCVAR_DEVELOPMENTONLY
+#endif
+
+ConVar hide_server( "hide_server", "0", FCVAR_GAMEDLL, "Whether the server should be hidden from the master server" );
+
+ConVar mp_waitingforplayers_time( "mp_waitingforplayers_time", (IsX360()?"15":"30"), FCVAR_GAMEDLL | WAITING_FOR_PLAYERS_FLAGS, "WaitingForPlayers time length in seconds" );
+
+ConVar tf_gamemode_arena ( "tf_gamemode_arena", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+ConVar tf_gamemode_cp ( "tf_gamemode_cp", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+ConVar tf_gamemode_ctf ( "tf_gamemode_ctf", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+ConVar tf_gamemode_sd ( "tf_gamemode_sd", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+ConVar tf_gamemode_rd ( "tf_gamemode_rd", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+ConVar tf_gamemode_pd ( "tf_gamemode_pd", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+ConVar tf_gamemode_tc ( "tf_gamemode_tc", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+ConVar tf_beta_content ( "tf_beta_content", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+ConVar tf_gamemode_payload ( "tf_gamemode_payload", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+ConVar tf_gamemode_mvm ( "tf_gamemode_mvm", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+ConVar tf_gamemode_passtime ( "tf_gamemode_passtime", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+ConVar tf_gamemode_misc ( "tf_gamemode_misc", "0", FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+
+ConVar tf_bot_count( "tf_bot_count", "0", FCVAR_NOTIFY | FCVAR_DEVELOPMENTONLY );
+
+#ifdef _DEBUG
+ConVar tf_debug_ammo_and_health( "tf_debug_ammo_and_health", "0", FCVAR_CHEAT );
+#endif // _DEBUG
+
+static Vector s_BotSpawnPosition;
+
+ConVar tf_gravetalk( "tf_gravetalk", "1", FCVAR_NOTIFY, "Allows living players to hear dead players using text/voice chat.", true, 0, true, 1 );
+
+ConVar tf_ctf_bonus_time ( "tf_ctf_bonus_time", "10", FCVAR_NOTIFY, "Length of team crit time for CTF capture." );
+
+#ifdef _DEBUG
+ConVar mp_scrambleteams_debug( "mp_scrambleteams_debug", "0", FCVAR_NONE, "Debug spew." );
+#endif // _DEBUG
+
+#ifdef STAGING_ONLY
+ConVar mp_tournament_readymode_bots_allowed( "mp_tournament_readymode_bots_allowed", "0", FCVAR_ARCHIVE, "Allow bot data to go through the system for debugging." );
+#endif // STAGING_ONLY
+
+extern ConVar tf_mm_servermode;
+extern ConVar tf_flag_caps_per_round;
+
+void cc_competitive_mode( IConVar *pConVar, const char *pOldString, float flOldValue )
+{
+ IGameEvent *event = gameeventmanager->CreateEvent( "competitive_state_changed" );
+ if ( event )
+ {
+ // Server-side here. Client-side down below in the RecvProxy
+ gameeventmanager->FireEvent( event, true );
+ }
+}
+ConVar tf_competitive_preround_duration( "tf_competitive_preround_duration", "3", FCVAR_REPLICATED, "How long we stay in pre-round when in competitive games." );
+ConVar tf_competitive_preround_countdown_duration( "tf_competitive_preround_countdown_duration", "10.5", FCVAR_HIDDEN, "How long we stay in countdown when in competitive games." );
+ConVar tf_competitive_abandon_method( "tf_competitive_abandon_method", "0", FCVAR_HIDDEN );
+ConVar tf_competitive_required_late_join_timeout( "tf_competitive_required_late_join_timeout", "120", FCVAR_DEVELOPMENTONLY,
+ "How long to wait for late joiners in matches requiring full player counts before canceling the match" );
+ConVar tf_competitive_required_late_join_confirm_timeout( "tf_competitive_required_late_join_confirm_timeout", "30", FCVAR_DEVELOPMENTONLY,
+ "How long to wait for the GC to confirm we're in the late join pool before canceling the match" );
+#endif // GAME_DLL
+
+#ifdef GAME_DLL
+void cc_powerup_mode( IConVar *pConVar, const char *pOldString, float flOldValue )
+{
+ ConVarRef var( pConVar );
+ if ( var.IsValid() )
+ {
+ if ( !TFGameRules() )
+ return;
+
+ if ( var.GetBool() )
+ {
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ return;
+ }
+
+ TFGameRules()->SetPowerupMode( var.GetBool() );
+ TFGameRules()->State_Transition( GR_STATE_PREROUND );
+ tf_flag_caps_per_round.SetValue( var.GetBool() ? 7 : 3 ); // Hack
+ }
+}
+
+ConVar tf_powerup_mode( "tf_powerup_mode", "0", FCVAR_NOTIFY, "Enable/disable powerup mode. Not compatible with Mann Vs Machine mode", cc_powerup_mode );
+ConVar tf_powerup_mode_imbalance_delta( "tf_powerup_mode_imbalance_delta", "24", FCVAR_CHEAT, "Powerup kill score lead one team must have before imbalance measures are initiated" );
+
+ConVar tf_skillrating_update_interval( "tf_skillrating_update_interval", "180", FCVAR_ARCHIVE, "How often to update the GC and OGS." );
+
+extern ConVar mp_teams_unbalance_limit;
+
+static bool g_bRandomMap = false;
+
+void cc_RandomMap( const CCommand& args )
+{
+ CTFGameRules *pRules = TFGameRules();
+ if ( pRules )
+ {
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ g_bRandomMap = true;
+ }
+ else
+ {
+ // There's no game rules object yet, so let's load the map cycle and pick a map.
+ char mapcfile[MAX_PATH];
+ CMultiplayRules::DetermineMapCycleFilename( mapcfile, sizeof(mapcfile), true );
+ if ( !mapcfile[0] )
+ {
+ Msg( "No mapcyclefile specified. Cannot pick a random map.\n" );
+ return;
+ }
+
+ CUtlVector<char*> mapList;
+ // No gamerules entity yet, since we don't need the fixups to find a map just use the base version
+ CMultiplayRules::RawLoadMapCycleFileIntoVector ( mapcfile, mapList );
+ if ( !mapList.Count() )
+ {
+ Msg( "Map cycle file \"%s\" contains no valid maps or cannot be read. Cannot pick a random map.\n", mapcfile );
+ return;
+ }
+
+ int iMapIndex = RandomInt( 0, mapList.Count() - 1 );
+ Msg ( "randommap: selecting map %i out of %i\n", iMapIndex + 1, mapList.Count() );
+ engine->ServerCommand( UTIL_VarArgs( "map %s\n", mapList[iMapIndex] ) );
+
+ CMultiplayRules::FreeMapCycleFileVector( mapList );
+ }
+}
+
+// Simple class for tracking previous gamemode across level transitions
+// Allows clean-up of UI/state when going between things like MvM and PvP
+class CTFGameModeHistory : public CAutoGameSystem
+{
+public:
+ virtual bool Init()
+ {
+ m_nPrevState = 0;
+ return true;
+ }
+ void SetPrevState( int nState ) { m_nPrevState = nState; }
+ int GetPrevState( void ) { return m_nPrevState; }
+private:
+ int m_nPrevState;
+} g_TFGameModeHistory;
+
+static ConCommand randommap( "randommap", cc_RandomMap, "Changelevel to a random map in the mapcycle file" );
+#endif // GAME_DLL
+
+#ifdef GAME_DLL
+static bool PlayerHasDuckStreaks( CTFPlayer *pPlayer )
+{
+ CEconItemView *pActionItem = pPlayer->GetEquippedItemForLoadoutSlot( LOADOUT_POSITION_ACTION );
+ if ( !pActionItem )
+ return false;
+
+ // Duck Badge Cooldown is based on badge level. Noisemaker is more like an easter egg
+ static CSchemaAttributeDefHandle pAttr_DuckStreaks( "duckstreaks active" );
+ uint32 iDuckStreaksActive = 0;
+
+ // Don't care about the level, just if the attribute is found
+ return FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pActionItem, pAttr_DuckStreaks, &iDuckStreaksActive ) && iDuckStreaksActive > 0;
+}
+
+void ValidateCapturesPerRound( IConVar *pConVar, const char *oldValue, float flOldValue )
+{
+ ConVarRef var( pConVar );
+
+ if ( var.GetInt() <= 0 )
+ {
+ // reset the flag captures being played in the current round
+ int nTeamCount = TFTeamMgr()->GetTeamCount();
+ for ( int iTeam = FIRST_GAME_TEAM; iTeam < nTeamCount; ++iTeam )
+ {
+ CTFTeam *pTeam = GetGlobalTFTeam( iTeam );
+ if ( !pTeam )
+ continue;
+
+ pTeam->SetFlagCaptures( 0 );
+ }
+ }
+}
+
+static void Coaching_Start( CTFPlayer *pCoach, CTFPlayer *pStudent )
+{
+ pCoach->SetIsCoaching( true );
+ pCoach->ForceChangeTeam( TEAM_SPECTATOR );
+ pCoach->SetObserverTarget( pStudent );
+ pCoach->StartObserverMode( OBS_MODE_CHASE );
+ pCoach->SetStudent( pStudent );
+ pStudent->SetCoach( pCoach );
+}
+
+static void Coaching_Stop( CTFPlayer *pCoach )
+{
+ CTFPlayer *pStudent = pCoach->GetStudent();
+ if ( pStudent )
+ {
+ pStudent->SetCoach( NULL );
+ }
+ pCoach->SetIsCoaching( false );
+ pCoach->SetStudent( NULL );
+ pCoach->ForceChangeTeam( TEAM_SPECTATOR );
+}
+
+#endif
+
+ConVar tf_flag_caps_per_round( "tf_flag_caps_per_round", "3", FCVAR_REPLICATED, "Number of captures per round on CTF and PASS Time maps. Set to 0 to disable.", true, 0, false, 0
+#ifdef GAME_DLL
+ , ValidateCapturesPerRound
+#endif
+ );
+
+
+/**
+ * Player hull & eye position for standing, ducking, etc. This version has a taller
+ * player height, but goldsrc-compatible collision bounds.
+ */
+static CViewVectors g_TFViewVectors(
+ Vector( 0, 0, 72 ), //VEC_VIEW (m_vView) eye position
+
+ Vector(-24, -24, 0 ), //VEC_HULL_MIN (m_vHullMin) hull min
+ Vector( 24, 24, 82 ), //VEC_HULL_MAX (m_vHullMax) hull max
+
+ Vector(-24, -24, 0 ), //VEC_DUCK_HULL_MIN (m_vDuckHullMin) duck hull min
+ Vector( 24, 24, 62 ), //VEC_DUCK_HULL_MAX (m_vDuckHullMax) duck hull max
+ Vector( 0, 0, 45 ), //VEC_DUCK_VIEW (m_vDuckView) duck view
+
+ Vector( -10, -10, -10 ), //VEC_OBS_HULL_MIN (m_vObsHullMin) observer hull min
+ Vector( 10, 10, 10 ), //VEC_OBS_HULL_MAX (m_vObsHullMax) observer hull max
+
+ Vector( 0, 0, 14 ) //VEC_DEAD_VIEWHEIGHT (m_vDeadViewHeight) dead view height
+);
+
+Vector g_TFClassViewVectors[11] =
+{
+ Vector( 0, 0, 72 ), // TF_CLASS_UNDEFINED
+
+ Vector( 0, 0, 65 ), // TF_CLASS_SCOUT, // TF_FIRST_NORMAL_CLASS
+ Vector( 0, 0, 75 ), // TF_CLASS_SNIPER,
+ Vector( 0, 0, 68 ), // TF_CLASS_SOLDIER,
+ Vector( 0, 0, 68 ), // TF_CLASS_DEMOMAN,
+ Vector( 0, 0, 75 ), // TF_CLASS_MEDIC,
+ Vector( 0, 0, 75 ), // TF_CLASS_HEAVYWEAPONS,
+ Vector( 0, 0, 68 ), // TF_CLASS_PYRO,
+ Vector( 0, 0, 75 ), // TF_CLASS_SPY,
+ Vector( 0, 0, 68 ), // TF_CLASS_ENGINEER,
+
+ Vector( 0, 0, 65 ), // TF_CLASS_CIVILIAN, // TF_LAST_NORMAL_CLASS
+};
+
+const CViewVectors *CTFGameRules::GetViewVectors() const
+{
+ return &g_TFViewVectors;
+}
+
+REGISTER_GAMERULES_CLASS( CTFGameRules );
+
+#ifdef CLIENT_DLL
+void RecvProxy_MatchSummary( const CRecvProxyData *pData, void *pStruct, void *pOut )
+{
+ bool bMatchSummary = ( pData->m_Value.m_Int > 0 );
+ if ( bMatchSummary && !(*(bool*)(pOut)))
+ {
+ C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer();
+ if ( pLocalPlayer )
+ {
+ pLocalPlayer->TurnOffTauntCam();
+ pLocalPlayer->TurnOffTauntCam_Finish();;
+ }
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "show_match_summary" );
+ if ( event )
+ {
+ gameeventmanager->FireEventClientSide( event );
+ }
+ }
+
+ *(bool*)(pOut) = bMatchSummary;
+}
+
+void RecvProxy_CompetitiveMode( const CRecvProxyData *pData, void *pStruct, void *pOut )
+{
+ *(bool*)(pOut) = ( pData->m_Value.m_Int > 0 );
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "competitive_state_changed" );
+ if ( event )
+ {
+ // Client-side once it's actually happened
+ gameeventmanager->FireEventClientSide( event );
+ }
+}
+
+void RecvProxy_PlayerVotedForMap( const CRecvProxyData *pData, void *pStruct, void *pOut )
+{
+ if ( *(int*)(pOut) != pData->m_Value.m_Int )
+ {
+ *(int*)(pOut) = pData->m_Value.m_Int;
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "player_next_map_vote_change" );
+ if ( event )
+ {
+ event->SetInt( "map_index", pData->m_Value.m_Int );
+ event->SetInt( "vote", pData->m_Value.m_Int );
+ // Client-side once it's actually happened
+ gameeventmanager->FireEventClientSide( event );
+ }
+ }
+}
+
+void RecvProxy_NewMapVoteStateChanged( const CRecvProxyData *pData, void *pStruct, void *pOut )
+{
+ bool bChange = *(int*)(pOut) != pData->m_Value.m_Int;
+ *(int*)(pOut) = pData->m_Value.m_Int;
+
+ if ( bChange )
+ {
+ IGameEvent *event = gameeventmanager->CreateEvent( "vote_maps_changed" );
+ if ( event )
+ {
+ // Client-side once it's actually happened
+ gameeventmanager->FireEventClientSide( event );
+ }
+ }
+}
+#endif
+
+BEGIN_NETWORK_TABLE_NOBASE( CTFGameRules, DT_TFGameRules )
+#ifdef CLIENT_DLL
+
+ RecvPropInt( RECVINFO( m_nGameType ) ),
+ RecvPropInt( RECVINFO( m_nStopWatchState ) ),
+ RecvPropString( RECVINFO( m_pszTeamGoalStringRed ) ),
+ RecvPropString( RECVINFO( m_pszTeamGoalStringBlue ) ),
+ RecvPropTime( RECVINFO( m_flCapturePointEnableTime ) ),
+
+//=============================================================================
+// HPE_BEGIN:
+// [msmith] Training status and HUD Type.
+//=============================================================================
+ RecvPropInt( RECVINFO( m_nHudType ) ),
+ RecvPropBool( RECVINFO( m_bIsInTraining ) ),
+ RecvPropBool( RECVINFO( m_bAllowTrainingAchievements ) ),
+ RecvPropBool( RECVINFO( m_bIsWaitingForTrainingContinue ) ),
+//=============================================================================
+// HPE_END
+//=============================================================================
+ RecvPropBool( RECVINFO( m_bIsTrainingHUDVisible ) ),
+ RecvPropBool( RECVINFO( m_bIsInItemTestingMode ) ),
+
+ RecvPropEHandle( RECVINFO( m_hBonusLogic ) ),
+ RecvPropBool( RECVINFO( m_bPlayingKoth ) ),
+ RecvPropBool( RECVINFO( m_bPlayingMedieval ) ),
+ RecvPropBool( RECVINFO( m_bPlayingHybrid_CTF_CP ) ),
+ RecvPropBool( RECVINFO( m_bPlayingSpecialDeliveryMode ) ),
+ RecvPropBool( RECVINFO( m_bPlayingRobotDestructionMode ) ),
+ RecvPropEHandle( RECVINFO( m_hRedKothTimer ) ),
+ RecvPropEHandle( RECVINFO( m_hBlueKothTimer ) ),
+ RecvPropInt( RECVINFO( m_nMapHolidayType ) ),
+
+ RecvPropEHandle( RECVINFO( m_itHandle ) ),
+ RecvPropBool( RECVINFO( m_bPlayingMannVsMachine ) ),
+ RecvPropEHandle( RECVINFO( m_hBirthdayPlayer ) ),
+
+ RecvPropInt( RECVINFO( m_nBossHealth ) ),
+ RecvPropInt( RECVINFO( m_nMaxBossHealth ) ),
+ RecvPropInt( RECVINFO( m_fBossNormalizedTravelDistance ) ),
+ RecvPropBool( RECVINFO( m_bMannVsMachineAlarmStatus ) ),
+ RecvPropBool( RECVINFO( m_bHaveMinPlayersToEnableReady ) ),
+
+ RecvPropBool( RECVINFO( m_bBountyModeEnabled ) ),
+
+ RecvPropInt( RECVINFO( m_nHalloweenEffect ) ),
+ RecvPropFloat( RECVINFO( m_fHalloweenEffectStartTime ) ),
+ RecvPropFloat( RECVINFO( m_fHalloweenEffectDuration ) ),
+ RecvPropInt( RECVINFO( m_halloweenScenario ) ),
+ RecvPropBool( RECVINFO( m_bHelltowerPlayersInHell ) ),
+ RecvPropBool( RECVINFO( m_bIsUsingSpells ) ),
+ RecvPropBool( RECVINFO( m_bCompetitiveMode ), 0, RecvProxy_CompetitiveMode ),
+ RecvPropInt( RECVINFO( m_nMatchGroupType ) ),
+ RecvPropBool( RECVINFO( m_bMatchEnded ) ),
+ RecvPropBool( RECVINFO( m_bPowerupMode ) ),
+ RecvPropString( RECVINFO( m_pszCustomUpgradesFile ) ),
+ RecvPropBool( RECVINFO( m_bTruceActive ) ),
+ RecvPropBool( RECVINFO( m_bShowMatchSummary ), 0, RecvProxy_MatchSummary ),
+ RecvPropBool( RECVINFO_NAME( m_bShowMatchSummary, "m_bShowCompetitiveMatchSummary" ), 0, RecvProxy_MatchSummary ), // Renamed
+ RecvPropBool( RECVINFO( m_bTeamsSwitched ) ),
+ RecvPropBool( RECVINFO( m_bMapHasMatchSummaryStage ) ),
+ RecvPropBool( RECVINFO( m_bPlayersAreOnMatchSummaryStage ) ),
+ RecvPropBool( RECVINFO( m_bStopWatchWinner ) ),
+ RecvPropArray3( RECVINFO_ARRAY(m_ePlayerWantsRematch), RecvPropInt( RECVINFO(m_ePlayerWantsRematch[0]), 0, RecvProxy_PlayerVotedForMap ) ),
+ RecvPropInt( RECVINFO( m_eRematchState ) ),
+ RecvPropArray3( RECVINFO_ARRAY(m_nNextMapVoteOptions), RecvPropInt( RECVINFO(m_nNextMapVoteOptions[0]), 0, RecvProxy_NewMapVoteStateChanged ) ),
+#else
+
+ SendPropInt( SENDINFO( m_nGameType ), 4, SPROP_UNSIGNED ),
+ SendPropInt( SENDINFO( m_nStopWatchState ), 3, SPROP_UNSIGNED ),
+ SendPropString( SENDINFO( m_pszTeamGoalStringRed ) ),
+ SendPropString( SENDINFO( m_pszTeamGoalStringBlue ) ),
+ SendPropTime( SENDINFO( m_flCapturePointEnableTime ) ),
+
+//=============================================================================
+// HPE_BEGIN:
+// [msmith] Training status and hud type.
+//=============================================================================
+ SendPropInt( SENDINFO( m_nHudType ), 3, SPROP_UNSIGNED ),
+ SendPropBool( SENDINFO( m_bIsInTraining ) ),
+ SendPropBool( SENDINFO( m_bAllowTrainingAchievements ) ),
+ SendPropBool( SENDINFO( m_bIsWaitingForTrainingContinue ) ),
+//=============================================================================
+// HPE_END
+//=============================================================================
+ SendPropBool( SENDINFO( m_bIsTrainingHUDVisible ) ),
+ SendPropBool( SENDINFO( m_bIsInItemTestingMode ) ),
+
+ SendPropEHandle( SENDINFO( m_hBonusLogic ) ),
+ SendPropBool( SENDINFO( m_bPlayingKoth ) ),
+ SendPropBool( SENDINFO( m_bPlayingMedieval ) ),
+ SendPropBool( SENDINFO( m_bPlayingHybrid_CTF_CP ) ),
+ SendPropBool( SENDINFO( m_bPlayingSpecialDeliveryMode ) ),
+ SendPropBool( SENDINFO( m_bPlayingRobotDestructionMode ) ),
+ SendPropEHandle( SENDINFO( m_hRedKothTimer ) ),
+ SendPropEHandle( SENDINFO( m_hBlueKothTimer ) ),
+ SendPropInt( SENDINFO( m_nMapHolidayType ), 3, SPROP_UNSIGNED ),
+
+ SendPropEHandle( SENDINFO( m_itHandle ) ),
+ SendPropBool( SENDINFO( m_bPlayingMannVsMachine ) ),
+ SendPropEHandle( SENDINFO( m_hBirthdayPlayer ) ),
+
+ SendPropInt( SENDINFO( m_nBossHealth ) ),
+ SendPropInt( SENDINFO( m_nMaxBossHealth ) ),
+ SendPropInt( SENDINFO( m_fBossNormalizedTravelDistance ) ),
+ SendPropBool( SENDINFO( m_bMannVsMachineAlarmStatus ) ),
+ SendPropBool( SENDINFO( m_bHaveMinPlayersToEnableReady ) ),
+
+ SendPropBool( SENDINFO( m_bBountyModeEnabled ) ),
+
+ SendPropInt( SENDINFO( m_nHalloweenEffect ) ),
+ SendPropFloat( SENDINFO( m_fHalloweenEffectStartTime ) ),
+ SendPropFloat( SENDINFO( m_fHalloweenEffectDuration ) ),
+ SendPropInt( SENDINFO( m_halloweenScenario ) ),
+ SendPropBool( SENDINFO( m_bHelltowerPlayersInHell ) ),
+ SendPropBool( SENDINFO( m_bIsUsingSpells ) ),
+ SendPropBool( SENDINFO( m_bCompetitiveMode ) ),
+ SendPropBool( SENDINFO( m_bPowerupMode ) ),
+ SendPropInt( SENDINFO( m_nMatchGroupType ) ),
+ SendPropBool( SENDINFO( m_bMatchEnded ) ),
+ SendPropString( SENDINFO( m_pszCustomUpgradesFile ) ),
+ SendPropBool( SENDINFO( m_bTruceActive ) ),
+ SendPropBool( SENDINFO( m_bShowMatchSummary ) ),
+ SendPropBool( SENDINFO( m_bTeamsSwitched ) ),
+ SendPropBool( SENDINFO( m_bMapHasMatchSummaryStage ) ),
+ SendPropBool( SENDINFO( m_bPlayersAreOnMatchSummaryStage ) ),
+ SendPropBool( SENDINFO( m_bStopWatchWinner ) ),
+ SendPropArray3( SENDINFO_ARRAY3(m_ePlayerWantsRematch), SendPropInt( SENDINFO_ARRAY(m_ePlayerWantsRematch), -1, SPROP_UNSIGNED | SPROP_VARINT ) ),
+ SendPropInt( SENDINFO( m_eRematchState ) ),
+ SendPropArray3( SENDINFO_ARRAY3(m_nNextMapVoteOptions), SendPropInt( SENDINFO_ARRAY(m_nNextMapVoteOptions), -1, SPROP_UNSIGNED | SPROP_VARINT ) ),
+#endif
+END_NETWORK_TABLE()
+
+LINK_ENTITY_TO_CLASS( tf_gamerules, CTFGameRulesProxy );
+IMPLEMENT_NETWORKCLASS_ALIASED( TFGameRulesProxy, DT_TFGameRulesProxy )
+
+#ifdef CLIENT_DLL
+ void RecvProxy_TFGameRules( const RecvProp *pProp, void **pOut, void *pData, int objectID )
+ {
+ CTFGameRules *pRules = TFGameRules();
+ Assert( pRules );
+ *pOut = pRules;
+ }
+
+ BEGIN_RECV_TABLE( CTFGameRulesProxy, DT_TFGameRulesProxy )
+ RecvPropDataTable( "tf_gamerules_data", 0, 0, &REFERENCE_RECV_TABLE( DT_TFGameRules ), RecvProxy_TFGameRules )
+ END_RECV_TABLE()
+#else
+ void *SendProxy_TFGameRules( const SendProp *pProp, const void *pStructBase, const void *pData, CSendProxyRecipients *pRecipients, int objectID )
+ {
+ CTFGameRules *pRules = TFGameRules();
+ Assert( pRules );
+ pRecipients->SetAllRecipients();
+ return pRules;
+ }
+
+ BEGIN_SEND_TABLE( CTFGameRulesProxy, DT_TFGameRulesProxy )
+ SendPropDataTable( "tf_gamerules_data", 0, &REFERENCE_SEND_TABLE( DT_TFGameRules ), SendProxy_TFGameRules )
+ END_SEND_TABLE()
+#endif
+
+#ifdef GAME_DLL
+BEGIN_DATADESC( CTFGameRulesProxy )
+//=============================================================================
+// HPE_BEGIN:
+// [msmith] Training HUD type
+//=============================================================================
+ DEFINE_KEYFIELD( m_nHudType, FIELD_INTEGER, "hud_type" ),
+//=============================================================================
+// HPE_END
+//=============================================================================
+
+ DEFINE_KEYFIELD( m_bOvertimeAllowedForCTF, FIELD_BOOLEAN, "ctf_overtime" ),
+
+ // Inputs.
+ DEFINE_INPUTFUNC( FIELD_FLOAT, "SetRedTeamRespawnWaveTime", InputSetRedTeamRespawnWaveTime ),
+ DEFINE_INPUTFUNC( FIELD_FLOAT, "SetBlueTeamRespawnWaveTime", InputSetBlueTeamRespawnWaveTime ),
+ DEFINE_INPUTFUNC( FIELD_FLOAT, "AddRedTeamRespawnWaveTime", InputAddRedTeamRespawnWaveTime ),
+ DEFINE_INPUTFUNC( FIELD_FLOAT, "AddBlueTeamRespawnWaveTime", InputAddBlueTeamRespawnWaveTime ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetRedTeamGoalString", InputSetRedTeamGoalString ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetBlueTeamGoalString", InputSetBlueTeamGoalString ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "SetRedTeamRole", InputSetRedTeamRole ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "SetBlueTeamRole", InputSetBlueTeamRole ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetRequiredObserverTarget", InputSetRequiredObserverTarget ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "AddRedTeamScore", InputAddRedTeamScore ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "AddBlueTeamScore", InputAddBlueTeamScore ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "SetRedKothClockActive", InputSetRedKothClockActive ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "SetBlueKothClockActive", InputSetBlueKothClockActive ),
+ DEFINE_INPUTFUNC( FIELD_FLOAT, "SetCTFCaptureBonusTime", InputSetCTFCaptureBonusTime ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "PlayVORed", InputPlayVORed ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "PlayVOBlue", InputPlayVOBlue ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "PlayVO", InputPlayVO ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "HandleMapEvent", InputHandleMapEvent ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetCustomUpgradesFile", InputSetCustomUpgradesFile ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "SetRoundRespawnFreezeEnabled", InputSetRoundRespawnFreezeEnabled ),
+ DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetMapForcedTruceDuringBossFight", InputSetMapForcedTruceDuringBossFight ),
+
+ DEFINE_OUTPUT( m_OnWonByTeam1, "OnWonByTeam1" ),
+ DEFINE_OUTPUT( m_OnWonByTeam2, "OnWonByTeam2" ),
+ DEFINE_OUTPUT( m_Team1PlayersChanged, "Team1PlayersChanged" ),
+ DEFINE_OUTPUT( m_Team2PlayersChanged, "Team2PlayersChanged" ),
+ DEFINE_OUTPUT( m_OnPowerupImbalanceTeam1, "OnPowerupImbalanceTeam1" ),
+ DEFINE_OUTPUT( m_OnPowerupImbalanceTeam2, "OnPowerupImbalanceTeam2" ),
+ DEFINE_OUTPUT( m_OnPowerupImbalanceMeasuresOver, "OnPowerupImbalanceMeasuresOver" ),
+ DEFINE_OUTPUT( m_OnStateEnterRoundRunning, "OnStateEnterRoundRunning" ),
+ DEFINE_OUTPUT( m_OnStateEnterBetweenRounds, "OnStateEnterBetweenRounds" ),
+ DEFINE_OUTPUT( m_OnStateEnterPreRound, "OnStateEnterPreRound" ),
+ DEFINE_OUTPUT( m_OnStateExitPreRound, "OnStateExitPreRound" ),
+ DEFINE_OUTPUT( m_OnMatchSummaryStart, "OnMatchSummaryStart" ),
+ DEFINE_OUTPUT( m_OnTruceStart, "OnTruceStart" ),
+ DEFINE_OUTPUT( m_OnTruceEnd, "OnTruceEnd" ),
+END_DATADESC()
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+CTFGameRulesProxy::CTFGameRulesProxy()
+{
+ m_nHudType = TF_HUDTYPE_UNDEFINED;
+ m_bOvertimeAllowedForCTF = true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputAddRedTeamScore( inputdata_t &inputdata )
+{
+ TFTeamMgr()->AddTeamScore( TF_TEAM_RED, inputdata.value.Int() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputAddBlueTeamScore( inputdata_t &inputdata )
+{
+ TFTeamMgr()->AddTeamScore( TF_TEAM_BLUE, inputdata.value.Int() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetRedKothClockActive( inputdata_t &inputdata )
+{
+ if ( TFGameRules() )
+ {
+ variant_t sVariant;
+ CTeamRoundTimer *pTimer = TFGameRules()->GetKothTeamTimer( TF_TEAM_BLUE );
+ if ( pTimer )
+ {
+ pTimer->AcceptInput( "Pause", NULL, NULL, sVariant, 0 );
+ }
+
+ pTimer = TFGameRules()->GetKothTeamTimer( TF_TEAM_RED );
+ if ( pTimer )
+ {
+ pTimer->AcceptInput( "Resume", NULL, NULL, sVariant, 0 );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetCTFCaptureBonusTime( inputdata_t &inputdata )
+{
+ if ( TFGameRules() )
+ {
+ TFGameRules()->SetCTFCaptureBonusTime( inputdata.value.Float() );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetBlueKothClockActive( inputdata_t &inputdata )
+{
+ if ( TFGameRules() )
+ {
+ variant_t sVariant;
+ CTeamRoundTimer *pTimer = TFGameRules()->GetKothTeamTimer( TF_TEAM_BLUE );
+ if ( pTimer )
+ {
+ pTimer->AcceptInput( "Resume", NULL, NULL, sVariant, 0 );
+ }
+
+ pTimer = TFGameRules()->GetKothTeamTimer( TF_TEAM_RED );
+ if ( pTimer )
+ {
+ pTimer->AcceptInput( "Pause", NULL, NULL, sVariant, 0 );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetRequiredObserverTarget( inputdata_t &inputdata )
+{
+ const char *pszEntName = inputdata.value.String();
+ CBaseEntity *pEnt = NULL;
+
+ if ( pszEntName && pszEntName[0] )
+ {
+ pEnt = gEntList.FindEntityByName( NULL, pszEntName );
+ }
+
+ TFGameRules()->SetRequiredObserverTarget( pEnt );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetRedTeamRespawnWaveTime( inputdata_t &inputdata )
+{
+ TFGameRules()->SetTeamRespawnWaveTime( TF_TEAM_RED, inputdata.value.Float() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetBlueTeamRespawnWaveTime( inputdata_t &inputdata )
+{
+ TFGameRules()->SetTeamRespawnWaveTime( TF_TEAM_BLUE, inputdata.value.Float() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputAddRedTeamRespawnWaveTime( inputdata_t &inputdata )
+{
+ TFGameRules()->AddTeamRespawnWaveTime( TF_TEAM_RED, inputdata.value.Float() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputAddBlueTeamRespawnWaveTime( inputdata_t &inputdata )
+{
+ TFGameRules()->AddTeamRespawnWaveTime( TF_TEAM_BLUE, inputdata.value.Float() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetRedTeamGoalString( inputdata_t &inputdata )
+{
+ TFGameRules()->SetTeamGoalString( TF_TEAM_RED, inputdata.value.String() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetBlueTeamGoalString( inputdata_t &inputdata )
+{
+ TFGameRules()->SetTeamGoalString( TF_TEAM_BLUE, inputdata.value.String() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetRedTeamRole( inputdata_t &inputdata )
+{
+ CTFTeam *pTeam = TFTeamMgr()->GetTeam( TF_TEAM_RED );
+ if ( pTeam )
+ {
+ pTeam->SetRole( inputdata.value.Int() );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetBlueTeamRole( inputdata_t &inputdata )
+{
+ CTFTeam *pTeam = TFTeamMgr()->GetTeam( TF_TEAM_BLUE );
+ if ( pTeam )
+ {
+ pTeam->SetRole( inputdata.value.Int() );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Pass in a VO sound entry to play for RED
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputPlayVORed( inputdata_t &inputdata )
+{
+ const char *szSoundName = inputdata.value.String();
+ if ( szSoundName )
+ {
+ TFGameRules()->BroadcastSound( TF_TEAM_RED, szSoundName );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Pass in a VO sound entry to play for BLUE
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputPlayVOBlue( inputdata_t &inputdata )
+{
+ const char *szSoundName = inputdata.value.String();
+ if ( szSoundName )
+ {
+ TFGameRules()->BroadcastSound( TF_TEAM_BLUE, szSoundName );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Pass in a VO sound entry to play
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputPlayVO( inputdata_t &inputdata )
+{
+ const char *szSoundName = inputdata.value.String();
+ if ( szSoundName )
+ {
+ TFGameRules()->BroadcastSound( 255, szSoundName );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputHandleMapEvent( inputdata_t &inputdata )
+{
+ if ( TFGameRules() )
+ {
+ TFGameRules()->HandleMapEvent( inputdata );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetCustomUpgradesFile( inputdata_t &inputdata )
+{
+ if ( TFGameRules() )
+ {
+ TFGameRules()->SetCustomUpgradesFile( inputdata );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetRoundRespawnFreezeEnabled( inputdata_t &inputdata )
+{
+ tf_player_movement_restart_freeze.SetValue( inputdata.value.Bool() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::InputSetMapForcedTruceDuringBossFight( inputdata_t &inputdata )
+{
+ if ( TFGameRules() )
+ {
+ TFGameRules()->SetMapForcedTruceDuringBossFight( inputdata.value.Bool() );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::Activate()
+{
+ TFGameRules()->Activate();
+
+//=============================================================================
+// HPE_BEGIN:
+// [msmith] We added a HUDTYPE so that the objective and the HUD are independent.
+// This lets us have a non training HUD on a training map. And a training
+// HUD on another type of map.
+//=============================================================================
+ if ( m_nHudType != TF_HUDTYPE_UNDEFINED )
+ {
+ TFGameRules()->SetHUDType( m_nHudType );
+ }
+//=============================================================================
+// HPE_END
+//=============================================================================
+
+ TFGameRules()->SetOvertimeAllowedForCTF( m_bOvertimeAllowedForCTF );
+
+ ListenForGameEvent( "teamplay_round_win" );
+
+ BaseClass::Activate();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::TeamPlayerCountChanged( CTFTeam *pTeam )
+{
+ if ( pTeam == TFTeamMgr()->GetTeam( TF_TEAM_RED ) )
+ {
+ m_Team1PlayersChanged.Set( pTeam->GetNumPlayers(), this, this );
+ }
+ else if ( pTeam == TFTeamMgr()->GetTeam( TF_TEAM_BLUE ) )
+ {
+ m_Team2PlayersChanged.Set( pTeam->GetNumPlayers(), this, this );
+ }
+
+ // Tell the clients
+ IGameEvent *event = gameeventmanager->CreateEvent( "teams_changed" );
+ if ( event )
+ {
+ gameeventmanager->FireEvent( event );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::PowerupTeamImbalance( int nTeam )
+{
+ switch ( nTeam )
+ {
+ case TF_TEAM_RED:
+ m_OnPowerupImbalanceTeam1.FireOutput( this, this );
+ break;
+ case TF_TEAM_BLUE:
+ m_OnPowerupImbalanceTeam2.FireOutput( this, this );
+ break;
+ default:
+ m_OnPowerupImbalanceMeasuresOver.FireOutput( this, this );
+ break;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::StateEnterRoundRunning( void )
+{
+ m_OnStateEnterRoundRunning.FireOutput( this, this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::StateEnterBetweenRounds( void )
+{
+ m_OnStateEnterBetweenRounds.FireOutput( this, this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::StateEnterPreRound( void )
+{
+ m_OnStateEnterPreRound.FireOutput( this, this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::StateExitPreRound( void )
+{
+ m_OnStateExitPreRound.FireOutput( this, this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::MatchSummaryStart( void )
+{
+ m_OnMatchSummaryStart.FireOutput( this, this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::TruceStart( void )
+{
+ m_OnTruceStart.FireOutput( this, this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::TruceEnd( void )
+{
+ m_OnTruceEnd.FireOutput( this, this );
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRulesProxy::FireGameEvent( IGameEvent *event )
+{
+#ifdef GAME_DLL
+ const char *pszEventName = event->GetName();
+
+ if ( FStrEq( pszEventName, "teamplay_round_win" ) )
+ {
+ int iWinningTeam = event->GetInt( "team" );
+
+ switch( iWinningTeam )
+ {
+ case TF_TEAM_RED:
+ m_OnWonByTeam1.FireOutput( this, this );
+ break;
+ case TF_TEAM_BLUE:
+ m_OnWonByTeam2.FireOutput( this, this );
+ break;
+ default:
+ break;
+ }
+ }
+#endif
+}
+
+// (We clamp ammo ourselves elsewhere).
+ConVar ammo_max( "ammo_max", "5000", FCVAR_REPLICATED );
+
+#ifndef CLIENT_DLL
+ConVar sk_plr_dmg_grenade( "sk_plr_dmg_grenade","0"); // Very lame that the base code needs this defined
+#endif
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : iDmgType -
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CTFGameRules::Damage_IsTimeBased( int iDmgType )
+{
+ // Damage types that are time-based.
+ return ( ( iDmgType & ( DMG_PARALYZE | DMG_NERVEGAS | DMG_DROWNRECOVER ) ) != 0 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : iDmgType -
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CTFGameRules::Damage_ShowOnHUD( int iDmgType )
+{
+ // Damage types that have client HUD art.
+ return ( ( iDmgType & ( DMG_DROWN | DMG_BURN | DMG_NERVEGAS | DMG_SHOCK ) ) != 0 );
+}
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : iDmgType -
+// Output : Returns true on success, false on failure.
+//-----------------------------------------------------------------------------
+bool CTFGameRules::Damage_ShouldNotBleed( int iDmgType )
+{
+ // Should always bleed currently.
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CTFGameRules::Damage_GetTimeBased( void )
+{
+ int iDamage = ( DMG_PARALYZE | DMG_NERVEGAS | DMG_DROWNRECOVER );
+ return iDamage;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CTFGameRules::Damage_GetShowOnHud( void )
+{
+ int iDamage = ( DMG_DROWN | DMG_BURN | DMG_NERVEGAS | DMG_SHOCK );
+ return iDamage;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CTFGameRules::Damage_GetShouldNotBleed( void )
+{
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
+// Return true if we are playing a PvE mode
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsPVEModeActive( void ) const
+{
+#ifdef TF_RAID_MODE
+ if ( IsRaidMode() || IsBossBattleMode() )
+ return true;
+#endif
+
+ if ( IsMannVsMachineMode() )
+ return true;
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Return true for PvE opponents (ie: enemy bot team)
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsPVEModeControlled( CBaseEntity *who ) const
+{
+ if ( !who )
+ {
+ return false;
+ }
+
+#ifdef GAME_DLL
+ if ( IsMannVsMachineMode() )
+ {
+ return who->GetTeamNumber() == TF_TEAM_PVE_INVADERS ? true : false;
+ }
+
+ if ( IsPVEModeActive() )
+ {
+ return who->GetTeamNumber() == TF_TEAM_RED ? true : false;
+ }
+#endif
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Return true if engineers can build quickly now
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsQuickBuildTime( void )
+{
+ return IsMannVsMachineMode() && ( InSetup() || TFObjectiveResource()->GetMannVsMachineIsBetweenWaves() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::GameModeUsesUpgrades( void )
+{
+ if ( IsMannVsMachineMode() || IsBountyMode() )
+ return true;
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CanPlayerUseRespec( CTFPlayer *pTFPlayer )
+{
+ if ( !pTFPlayer )
+ return false;
+
+ bool bAllowed = IsMannVsMachineRespecEnabled() && State_Get() == GR_STATE_BETWEEN_RNDS;
+
+#ifdef GAME_DLL
+ if ( !g_pPopulationManager )
+ return false;
+
+ bAllowed &= ( g_pPopulationManager->GetNumRespecsAvailableForPlayer( pTFPlayer ) ) ? true : false;
+#else
+ if ( !g_TF_PR )
+ return false;
+
+ bAllowed &= ( g_TF_PR->GetNumRespecCredits( pTFPlayer->entindex() ) ) ? true : false;
+#endif
+
+ return bAllowed;
+}
+
+bool CTFGameRules::IsCompetitiveMode( void ) const
+{
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc )
+ {
+ return pMatchDesc->m_params.m_eMatchType == MATCH_TYPE_COMPETITIVE
+ || pMatchDesc->m_params.m_eMatchType == MATCH_TYPE_CASUAL;
+ }
+
+ return false;
+}
+
+bool CTFGameRules::IsMatchTypeCasual( void ) const
+{
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc )
+ {
+ return ( pMatchDesc->m_params.m_eMatchType == MATCH_TYPE_CASUAL );
+ }
+
+ return false;
+}
+
+bool CTFGameRules::IsMatchTypeCompetitive( void ) const
+{
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc )
+ {
+ return ( pMatchDesc->m_params.m_eMatchType == MATCH_TYPE_COMPETITIVE );
+ }
+
+ return false;
+}
+
+bool CTFGameRules::BInMatchStartCountdown() const
+{
+ if ( IsCompetitiveMode() )
+ {
+ float flTime = GetRoundRestartTime();
+ if ( ( flTime > 0.f ) && ( (int)( flTime - gpGlobals->curtime ) <= mp_tournament_readymode_countdown.GetInt() ) )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+EMatchGroup CTFGameRules::GetCurrentMatchGroup() const
+{
+#if defined STAGING_ONLY && defined CLIENT_DLL
+ if ( tf_fake_mm_group.GetInt() != -1 )
+ {
+ return (EMatchGroup)tf_fake_mm_group.GetInt();
+ }
+#endif
+
+#ifdef GAME_DLL
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ return pMatch ? pMatch->m_eMatchGroup : k_nMatchGroup_Invalid;
+#else
+ // Client
+ // We only care about what the server says if we are in an MM match. We pass false
+ // into BConnectedToMatch because we want the match group of the server EVEN IF
+ // the match is over, but we're still connected.
+ return GTFGCClientSystem()->BConnectedToMatchServer( false ) ? (EMatchGroup)m_nMatchGroupType.Get() : k_nMatchGroup_Invalid;
+#endif
+}
+
+bool CTFGameRules::IsManagedMatchEnded() const
+{
+#ifdef GAME_DLL
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ return !pMatch || pMatch->BMatchTerminated();
+#else
+ // Client
+ // We only care about what the server says if we are in an MM match. (Note that the GC client system calls this to
+ // determine if an MM server considers the match over, so beware circular logic)
+ return !GTFGCClientSystem()->BConnectedToMatchServer( true ) || m_bMatchEnded;
+#endif
+}
+
+#ifdef GAME_DLL
+//-----------------------------------------------------------------------------
+void CTFGameRules::SyncMatchSettings()
+{
+ // These mirror the MatchInfo for the client's sake.
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+
+ m_nMatchGroupType.Set( pMatch ? pMatch->m_eMatchGroup : k_nMatchGroup_Invalid );
+ m_bMatchEnded.Set( IsManagedMatchEnded() );
+}
+
+//-----------------------------------------------------------------------------
+bool CTFGameRules::StartManagedMatch()
+{
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ if ( !pMatch )
+ {
+ Warning( "Starting a managed match with no match info\n" );
+ return false;
+ }
+
+ // Cleanup
+ m_eRematchState = NEXT_MAP_VOTE_STATE_NONE;
+
+ /// Sync these before level change, so there's no race condition where clients may connect during/before the
+ /// changelevel and see that the match is ended or wrong.
+ SyncMatchSettings();
+
+ // Change the the correct map from the match. If no match specified, perform a fresh load of the current map
+ const char *pszMap = pMatch->GetMatchMap();
+ if ( !pszMap )
+ {
+ pszMap = STRING( gpGlobals->mapname );
+ Warning( "Managed match did not specify map, using current map (%s)\n", pszMap );
+ }
+
+ engine->ServerCommand( CFmtStr( "changelevel %s\n", pszMap ) );
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetCompetitiveMode( bool bValue )
+{
+ m_bCompetitiveMode.Set( bValue );
+ //// Competitive mode is only supported on official servers.
+ //// It requires matchmaking, and doesn't allow ad-hoc connections.
+ //// If cheats are ever enabled, we force this mode off.
+
+ //tf_mm_trusted.SetValue( bValue );
+ //mp_tournament.SetValue( bValue );
+ //mp_tournament_readymode.SetValue( bValue );
+
+ //// No ad-hoc connections
+ //tf_mm_strict.SetValue( bValue );
+
+ //if ( bValue )
+ //{
+ // engine->ServerCommand( "exec server_ladder.cfg\n" );
+
+ // Assert( tf_mm_servermode.GetInt() == 1 );
+ //}
+
+ //// Any state toggle is a reset (so don't spam this call)
+ //State_Transition( GR_STATE_PREROUND );
+ //SetInWaitingForPlayers( bValue );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::StartCompetitiveMatch( void )
+{
+ m_flSafeToLeaveTimer = -1.f;
+
+ SetInWaitingForPlayers( false );
+ RoundRespawn();
+ State_Transition( GR_STATE_RESTART );
+ ResetPlayerAndTeamReadyState();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Stops a match for some specified reason
+//-----------------------------------------------------------------------------
+void CTFGameRules::StopCompetitiveMatch( CMsgGC_Match_Result_Status nCode )
+{
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ int nActiveMatchPlayers = pMatch->GetNumActiveMatchPlayers();
+ if ( BAttemptMapVoteRollingMatch() &&
+ nCode == CMsgGC_Match_Result_Status_MATCH_SUCCEEDED &&
+ nActiveMatchPlayers > 0 )
+ {
+ ChooseNextMapVoteOptions();
+ }
+ else
+ {
+ // If we're not attempting a rolling match, end it
+ // TODO ROLLING MATCHES: If we bail between now and RequestNewMatchForLobby, we need to call this or we'll get stuck.
+ if ( !IsManagedMatchEnded() )
+ {
+ GTFGCClientSystem()->EndManagedMatch( /* bKickPlayersToParties */ false );
+ Assert( IsManagedMatchEnded() );
+ m_bMatchEnded.Set( true );
+ }
+ }
+
+ if ( nCode == CMsgGC_Match_Result_Status_MATCH_SUCCEEDED )
+ {
+ IGameEvent *winEvent = gameeventmanager->CreateEvent( "competitive_victory" );
+ if ( winEvent )
+ {
+ gameeventmanager->FireEvent( winEvent );
+ }
+
+ //
+ // This determines new ratings and creates the GC messages
+ //
+
+ CMatchInfo *pInfo = GTFGCClientSystem()->GetMatch();
+ if ( pInfo )
+ {
+ pInfo->CalculateMatchSkillRatingAdjustments( m_iWinningTeam );
+
+ // Performance ranking with medals is currently server-side
+ if ( pInfo->CalculatePlayerMatchRankData() )
+ {
+ // Send scoreboard event with final data
+ int nPlayers = pInfo->GetNumTotalMatchPlayers();
+ for ( int idx = 0; idx < nPlayers; idx++ )
+ {
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "competitive_stats_update" );
+ if ( pEvent )
+ {
+ CMatchInfo::PlayerMatchData_t *pMatchRankData = pInfo->GetMatchDataForPlayer( idx );
+
+ CBasePlayer *pPlayer = UTIL_PlayerBySteamID( pMatchRankData->steamID );
+ if ( !pPlayer )
+ continue;
+
+ pEvent->SetInt( "index", pPlayer->entindex() );
+ pEvent->SetInt( "score_rank", pMatchRankData ? pMatchRankData->nScoreMedal : 0 ); // medal won (if any)
+ pEvent->SetInt( "kills_rank", pMatchRankData ? pMatchRankData->nKillsMedal : 0 ); //
+ pEvent->SetInt( "damage_rank", pMatchRankData ? pMatchRankData->nDamageMedal : 0 ); //
+ pEvent->SetInt( "healing_rank", pMatchRankData ? pMatchRankData->nHealingMedal : 0 ); //
+ pEvent->SetInt( "support_rank", pMatchRankData ? pMatchRankData->nSupportMedal : 0 ); //
+ gameeventmanager->FireEvent( pEvent );
+ }
+ }
+ }
+ }
+ else
+ {
+ Warning( "CalculatePlayerMatchRankData(): General failure (investigate).\n" );
+ }
+
+ ReportMatchResultsToGC( nCode );
+ }
+ else if ( nCode == CMsgGC_Match_Result_Status_MATCH_FAILED_ABANDON )
+ {
+ // This generates a "safe to leave" notification on clients
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "player_abandoned_match" );
+ if ( pEvent )
+ {
+ pEvent->SetBool( "game_over", ( tf_competitive_abandon_method.GetBool() || State_Get() == GR_STATE_BETWEEN_RNDS ) );
+ gameeventmanager->FireEvent( pEvent );
+ }
+
+ ReportMatchResultsToGC( nCode );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Fully completes the match
+//-----------------------------------------------------------------------------
+void CTFGameRules::EndCompetitiveMatch( void )
+{
+ MatchSummaryEnd();
+
+ Log( "Competitive match ended. Kicking all players.\n" );
+ engine->ServerCommand( "kickall #TF_Competitive_Disconnect\n" );
+
+ // Prepare for next match
+ g_fGameOver = false;
+ m_bAllowBetweenRounds = true;
+ State_Transition( GR_STATE_RESTART );
+ SetInWaitingForPlayers( true );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Called during CTFGameRules::Think()
+//-----------------------------------------------------------------------------
+void CTFGameRules::ManageCompetitiveMode( void )
+{
+ if ( !IsCompetitiveMode() )
+ return;
+
+ // Bring this back when we ship?
+// // Security check
+// if ( !tf_skillrating_debug.GetBool() )
+// {
+// m_bCompetitiveMode &= tf_mm_trusted.GetBool() &&
+// IsInTournamentMode() &&
+// !HaveCheatsBeenEnabledDuringLevel();
+// }
+
+ // We lost trusted status
+ if ( !tf_mm_trusted.GetBool() )
+ {
+ m_nMatchGroupType.Set( k_nMatchGroup_Invalid );
+ StopCompetitiveMatch( CMsgGC_Match_Result_Status_MATCH_FAILED_TRUSTED );
+ UTIL_ClientPrintAll( HUD_PRINTCENTER, "Exiting Competitive Mode!" );
+ Log( "Server lost trusted status. Exiting Competitive Mode!" );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ReportMatchResultsToGC( CMsgGC_Match_Result_Status nCode )
+{
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ if ( !pMatch )
+ return false;
+
+ GCSDK::CProtoBufMsg< CMsgGC_Match_Result > *pMsg = new GCSDK::CProtoBufMsg< CMsgGC_Match_Result >( k_EMsgGC_Match_Result );
+
+ pMsg->Body().set_match_id( pMatch->m_nMatchID );
+ pMsg->Body().set_match_group( pMatch->m_eMatchGroup );
+ pMsg->Body().set_status( nCode );
+ pMsg->Body().set_duration( CTF_GameStats.m_currentMap.m_Header.m_iTotalTime + ( gpGlobals->curtime - m_flRoundStartTime ) );
+
+ CTeam *pTeam = GetGlobalTeam( TF_TEAM_RED );
+ pMsg->Body().set_red_score( pTeam ? pTeam->GetScore() : (uint32)-1 );
+ pTeam = GetGlobalTeam( TF_TEAM_BLUE );
+ pMsg->Body().set_blue_score( pTeam ? pTeam->GetScore() : (uint32)-1 );
+ Assert( m_iWinningTeam >= 0 );
+ pMsg->Body().set_winning_team( Max( 0, (int)m_iWinningTeam ) );
+ const MapDef_t *pMap = GetItemSchema()->GetMasterMapDefByName( STRING( gpGlobals->mapname ) );
+ pMsg->Body().set_map_index( ( pMap ) ? pMap->m_nDefIndex : 0 );
+ pMsg->Body().set_game_type( 1 ); // TODO: eMapGameType
+
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( pMatch->m_eMatchGroup );
+ if( !pMatchDesc || !pMatchDesc->m_pProgressionDesc )
+ return false;
+
+ int nTotalScore = 0;
+
+ CTFPlayerResource *pTFResource = dynamic_cast< CTFPlayerResource* >( g_pPlayerResource );
+ if ( pTFResource )
+ {
+ pTFResource->UpdatePlayerData();
+
+ for ( int i=0; i < MAX_PLAYERS; ++i )
+ {
+ nTotalScore += pTFResource->GetTotalScore( i );
+ }
+ }
+
+ float flBlueScoreRatio = 0.5f;
+
+ const CTFTeam* pTFTeamRed = GetGlobalTFTeam( TF_TEAM_RED );
+ const CTFTeam* pTFTeamBlue = GetGlobalTFTeam( TF_TEAM_BLUE );
+
+ // Figure out how much XP to give each team based on the game mode played
+ if ( HasMultipleTrains() )
+ {
+ // In PLR we want to use the distance along the tracks each of the
+ // trains were at the end of each round
+ flBlueScoreRatio = RemapValClamped( pTFTeamBlue->GetTotalPLRTrackPercentTraveled(), 0.f, pTFTeamBlue->GetTotalPLRTrackPercentTraveled() + pTFTeamRed->GetTotalPLRTrackPercentTraveled(), 0.f, 1.f );
+ }
+ else if ( !m_bPlayingKoth && !m_bPowerupMode && ( tf_gamemode_cp.GetInt() || tf_gamemode_sd.GetInt() || tf_gamemode_payload.GetInt() ) )
+ {
+ // Rounds Won
+ // CP - Points can flow back and forever, so we can't count total caps. And the
+ // state of the game is always the same at match end, so rounds won is our
+ // only real metric.
+ // SD - A flag cap is a round, so we could count caps or rounds here. Again, the
+ // flag can go back and forth forever, but the match end state is always the same.
+ // PL - Count the points captured by each team. We do this A/D style where each team has
+ // a chance to score points.
+ flBlueScoreRatio = RemapValClamped( pTFTeamBlue->GetScore(), 0.f, pTFTeamBlue->GetScore() + pTFTeamRed->GetScore() , 0.f, 1.f );
+ }
+ else if ( tf_gamemode_ctf.GetInt() || m_bPowerupMode || tf_gamemode_passtime.GetInt() )
+ {
+ // Flag captures
+ // Mannpower is a variant of CTF and Passtime effectively is CTF. In all of these modes
+ // we don't use rounds so our best metric is individual flag captures.
+ flBlueScoreRatio = RemapValClamped( pTFTeamBlue->GetTotalFlagCaptures(), 0.f, pTFTeamBlue->GetTotalFlagCaptures() + pTFTeamRed->GetTotalFlagCaptures(), 0.f, 1.f );
+ }
+ else if ( m_bPlayingKoth )
+ {
+ // Time capped
+ // Looking at the capped time for each team will let us give xp in a fair way. We can
+ // actually get as close as 50/50
+ float flBlueTimeCapped = pTFTeamBlue->GetKOTHTime();
+ float flRedTiemCapped = pTFTeamRed->GetKOTHTime();
+
+ flBlueScoreRatio = RemapValClamped( flBlueTimeCapped, 0.f, flBlueTimeCapped + flRedTiemCapped, 0.f, 1.f );
+ }
+ else if ( tf_gamemode_rd.GetInt() || tf_gamemode_pd.GetInt() )
+ {
+ CTFRobotDestructionLogic* pRDLogic = CTFRobotDestructionLogic::GetRobotDestructionLogic();
+
+ // Count bottles/cores scored by each team
+ flBlueScoreRatio = RemapValClamped( pRDLogic->GetScore( TF_TEAM_BLUE ), 0.f, pRDLogic->GetScore( TF_TEAM_BLUE ) + pRDLogic->GetScore( TF_TEAM_RED ), 0.f, 1.f );
+ }
+ else if ( tf_gamemode_tc.GetInt() )
+ {
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+
+ int nBlueMiniRounds = 0;
+ int nRedMiniRounds = 0;
+
+ // Use the number of mini-rounds won by each team
+ for( int i=0; i < pMaster->GetNumRounds(); ++ i )
+ {
+ const CTeamControlPointRound* pRound = pMaster->GetRoundByIndex( i );
+ if ( pRound->RoundOwnedByTeam( TF_TEAM_RED ) )
+ {
+ ++nRedMiniRounds;
+ }
+ else if ( pRound->RoundOwnedByTeam( TF_TEAM_BLUE ) )
+ {
+ ++nBlueMiniRounds;
+ }
+ else
+ {
+ Assert( false );
+ }
+ }
+
+
+ flBlueScoreRatio = RemapValClamped( nBlueMiniRounds, 0.f, nBlueMiniRounds + nRedMiniRounds, 0.f, 1.f );
+ }
+ else
+ {
+ Assert( !"Game mode not handled for team XP bonus!" );
+ }
+
+ const float flRedScoreRatio = 1.f - flBlueScoreRatio;
+ const int nBlueTeamObjectiveBonus = flBlueScoreRatio * nTotalScore;
+ const int nRedTeamObjectiveBonus = flRedScoreRatio * nTotalScore;
+
+ // Player info
+ for ( int idxPlayer = 0; idxPlayer < pMatch->GetNumTotalMatchPlayers(); idxPlayer++ )
+ {
+ CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( idxPlayer );
+ if ( !pMatchPlayer->steamID.BIndividualAccount() )
+ {
+ Assert( false );
+ continue;
+ }
+
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerBySteamID( pMatchPlayer->steamID ) );
+ PlayerStats_t *pStats = CTF_GameStats.FindPlayerStats( pTFPlayer );
+
+ CMsgGC_Match_Result_Player *pMsgPlayer = pMsg->Body().add_players();
+ int nTeam = GetGameTeamForGCTeam( pMatchPlayer->eGCTeam );
+ pMsgPlayer->set_steam_id( pMatchPlayer->steamID.ConvertToUint64() );
+ pMsgPlayer->set_team( nTeam );
+ if ( pTFResource && pTFPlayer )
+ {
+ pMsgPlayer->set_score( pTFResource->GetTotalScore( pTFPlayer->entindex() ) );
+ }
+ else
+ {
+ // They left
+ }
+
+ int nPing = 0;
+ int nPacketLoss = 0;
+ if ( pTFPlayer )
+ {
+ UTIL_GetPlayerConnectionInfo( pTFPlayer->entindex(), nPing, nPacketLoss );
+ }
+ pMsgPlayer->set_ping( nPing );
+ uint32 unPlayerFlags = 0U;
+ if ( pMatchPlayer->bDropped )
+ {
+ unPlayerFlags |= MATCH_FLAG_PLAYER_LEAVER;
+ }
+ if ( pMatchPlayer->bLateJoin )
+ {
+ unPlayerFlags |= MATCH_FLAG_PLAYER_LATEJOIN;
+ }
+ if ( pMatchPlayer->BDropWasAbandon() )
+ {
+ unPlayerFlags |= MATCH_FLAG_PLAYER_ABANDONER;
+ }
+ if ( pMatchPlayer->bPlayed )
+ {
+ unPlayerFlags |= MATCH_FLAG_PLAYER_PLAYED;
+ }
+
+ pMsgPlayer->set_flags( unPlayerFlags );
+ // server-side skill system
+ FixmeMMRatingBackendSwapping(); // Assuming skill rating is drillo
+ pMsgPlayer->set_classes_played( pMatchPlayer->unClassesPlayed );
+ pMsgPlayer->set_kills( pStats ? pStats->statsAccumulated.m_iStat[TFSTAT_KILLS] : 0 );
+ pMsgPlayer->set_damage( pStats ? pStats->statsAccumulated.m_iStat[TFSTAT_DAMAGE] : 0 );
+ pMsgPlayer->set_healing( pStats ? pStats->statsAccumulated.m_iStat[TFSTAT_HEALING] : 0 );
+ pMsgPlayer->set_support( pStats ? CalcPlayerSupportScore( &pStats->statsAccumulated, pTFPlayer->entindex() ) : 0 );
+ pMsgPlayer->set_score_medal( pMatchPlayer->nScoreMedal );
+ pMsgPlayer->set_kills_medal( pMatchPlayer->nKillsMedal );
+ pMsgPlayer->set_damage_medal( pMatchPlayer->nDamageMedal );
+ pMsgPlayer->set_healing_medal( pMatchPlayer->nHealingMedal );
+ pMsgPlayer->set_support_medal( pMatchPlayer->nSupportMedal );
+ FixmeMMRatingBackendSwapping(); // Assuming we're using skill rating for rank? Why even include this?
+ pMsgPlayer->set_rank( pMatchDesc->m_pProgressionDesc->GetLevelForExperience( pMatchPlayer->unMMSkillRating ).m_nLevelNum );
+ pMsgPlayer->set_deaths( pStats ? pStats->statsAccumulated.m_iStat[TFSTAT_DEATHS] : 0 );
+ pMsgPlayer->set_party_id( pMatchPlayer->uPartyID );
+ uint32 unLeaveTime = ( pMatchPlayer && ( pMatchPlayer->bDropped || pMatchPlayer->BDropWasAbandon() ) ) ?
+ pMatchPlayer->GetLastActiveEventTime() : 0u;
+ pMsgPlayer->set_leave_time( unLeaveTime );
+ pMsgPlayer->set_leave_reason( pMatchPlayer->GetDropReason() );
+ pMsgPlayer->set_connect_time( pMatchPlayer->rtJoinedMatch );
+
+ // Somebody won! Match finish bonus
+ if ( nCode == CMsgGC_Match_Result_Status_MATCH_SUCCEEDED )
+ {
+ // Give points based on team performance
+ int nPerformanceScore = RemapValClamped( pMsgPlayer->score(), 0, nTotalScore / 24, 0, nTeam == TF_TEAM_RED ? nRedTeamObjectiveBonus : nBlueTeamObjectiveBonus );
+ pMatch->GiveXPRewardToPlayerForAction( pMatchPlayer->steamID, CMsgTFXPSource::SOURCE_OBJECTIVE_BONUS, nPerformanceScore );
+
+ // Everyone gets base completion points
+ int nCompletionScore = RemapValClamped( pMsgPlayer->score(), 0, nTotalScore / 24, 0, nTotalScore );
+ pMatch->GiveXPRewardToPlayerForAction( pMatchPlayer->steamID, CMsgTFXPSource::SOURCE_COMPLETED_MATCH, nCompletionScore );
+ }
+
+ // Copy any pending XP sources they had ready to send up
+ for( int i=0; i < pMatchPlayer->GetXPSources().sources_size(); ++i )
+ {
+ CMsgTFXPSource* pXPSource = pMsgPlayer->add_xp_breakdown();
+ pXPSource->CopyFrom( pMatchPlayer->GetXPSources().sources( i ) );
+ }
+ }
+
+ pMsg->Body().set_win_reason( GetWinReason() );
+ uint32 unMatchFlags = 0u;
+
+ if ( pMatch->m_uLobbyFlags & LOBBY_FLAG_LOWPRIORITY )
+ {
+ unMatchFlags |= MATCH_FLAG_LOWPRIORITY;
+ }
+
+ if ( pMatch->m_uLobbyFlags & LOBBY_FLAG_REMATCH )
+ {
+ unMatchFlags |= MATCH_FLAG_REMATCH;
+ }
+
+ pMsg->Body().set_flags( unMatchFlags );
+ pMsg->Body().set_bots( pMatch->m_nBotsAdded );
+
+ GTFGCClientSystem()->SendCompetitiveMatchResult( pMsg );
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::MatchmakingShouldUseStopwatchMode()
+{
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ bool bRetVal = !HasMultipleTrains() && ( tf_gamemode_payload.GetBool() || ( pMaster && ( pMaster->PlayingMiniRounds() || pMaster->ShouldSwitchTeamsOnRoundWin() ) ) );
+
+ tf_attack_defend_map.SetValue( bRetVal );
+ return bRetVal;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetPowerupMode( bool bValue )
+{
+ // Powerup mode uses grapple and changes some gamerule variables.
+
+ if ( bValue )
+ {
+ tf_grapplinghook_enable.SetValue( 1 );
+ tf_flag_return_time_credit_factor.SetValue( 0 );
+ }
+ else
+ {
+ tf_grapplinghook_enable.SetValue( 0 );
+ tf_flag_return_time_credit_factor.SetValue( 1 );
+ }
+
+ m_bPowerupMode = bValue;
+}
+
+#ifdef GAME_DLL
+//-----------------------------------------------------------------------------
+void CTFGameRules::EndManagedMvMMatch( bool bKickPlayersToParties )
+{
+ // Primarily a pass through so we can ensure our match end state is sync'd -- CPopulationManager manages most of the
+ // MvM meta round state
+ if ( !IsManagedMatchEnded() )
+ {
+ GTFGCClientSystem()->EndManagedMatch( bKickPlayersToParties );
+ Assert( IsManagedMatchEnded() );
+ m_bMatchEnded.Set( true );
+ }
+}
+#endif // GAME_DLL
+
+
+#ifdef STAGING_ONLY
+//-----------------------------------------------------------------------------
+// Purpose: Enable/Disable Bounty Mode
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetBountyMode( bool bValue )
+{
+ if ( m_bBountyModeEnabled.Get() != bValue )
+ {
+ m_bBountyModeEnabled.Set( bValue );
+ }
+
+ // If enabling, dynamically create an upgrade entity
+ if ( bValue )
+ {
+ if ( !g_hUpgradeEntity && !m_pUpgrades )
+ {
+ m_pUpgrades = CBaseEntity::Create( "func_upgradestation", vec3_origin, vec3_angle );
+ }
+ }
+
+ // If disabling, remove upgrade entity
+ if ( !bValue && m_pUpgrades )
+ {
+ UTIL_Remove( m_pUpgrades );
+ m_pUpgrades = NULL;
+ }
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "bountymode_toggled" );
+ if ( event )
+ {
+ event->SetBool( "active", bValue );
+ gameeventmanager->FireEvent( event );
+ }
+}
+
+#endif // GAME_DLL
+#endif // STAGING_ONLY
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::UsePlayerReadyStatusMode( void )
+{
+ if ( IsMannVsMachineMode() )
+ return true;
+
+ if ( IsCompetitiveMode() )
+ return true;
+
+ if ( mp_tournament.GetBool() && mp_tournament_readymode.GetBool() )
+ return true;
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::PlayerReadyStatus_HaveMinPlayersToEnable( void )
+{
+ // we always have enough players if the match wants players to autoready
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc && pMatchDesc->m_params.m_bAutoReady )
+ return true;
+
+#ifdef GAME_DLL
+ // Count connected players
+ int nNumPlayers = 0;
+ CUtlVector< CTFPlayer* > playerVector;
+ CollectPlayers( &playerVector );
+ FOR_EACH_VEC( playerVector, i )
+ {
+ if ( !playerVector[i] )
+ continue;
+
+ if ( playerVector[i]->IsFakeClient() )
+ continue;
+
+#ifdef STAGING_ONLY
+ if ( !mp_tournament_readymode_bots_allowed.GetBool() && playerVector[i]->IsBot() )
+ continue;
+#else
+ if ( playerVector[i]->IsBot() )
+ continue;
+#endif
+
+ if ( playerVector[i]->IsHLTV() )
+ continue;
+
+ if ( playerVector[i]->IsReplay() )
+ continue;
+
+ nNumPlayers++;
+ }
+
+ // Default
+ int nMinPlayers = 1;
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+
+ if ( pMatch && !pMatch->BMatchTerminated() && pMatchDesc->m_params.m_bRequireCompleteMatch )
+ {
+ nMinPlayers = pMatch->GetCanonicalMatchSize();
+ }
+ else if ( IsMannVsMachineMode() &&
+ ( engine->IsDedicatedServer() || ( !engine->IsDedicatedServer() && nNumPlayers > 1 ) ) )
+ {
+ nMinPlayers = tf_mvm_min_players_to_start.GetInt();
+ }
+ else if ( UsePlayerReadyStatusMode() && engine->IsDedicatedServer() )
+ {
+ nMinPlayers = mp_tournament_readymode_min.GetInt();
+ }
+
+ // Should be renamed to m_bEnableReady, not sure why we encoded our criteria in the names of all associated functions and variables...
+ m_bHaveMinPlayersToEnableReady.Set( nNumPlayers >= nMinPlayers );
+
+#endif
+
+ return m_bHaveMinPlayersToEnableReady;
+}
+
+#ifdef GAME_DLL
+//-----------------------------------------------------------------------------
+bool CTFGameRules::PlayerReadyStatus_ArePlayersOnTeamReady( int iTeam )
+{
+ if ( IsMannVsMachineMode() && iTeam == TF_TEAM_PVE_INVADERS )
+ return true;
+
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ if ( pMatch )
+ {
+ int nMatchPlayers = pMatch->GetNumTotalMatchPlayers();
+ if ( nMatchPlayers <= 0 )
+ return false;
+
+ int iPlayerReadyCount = 0;
+ for ( int i = 0; i < nMatchPlayers; i++ )
+ {
+ CMatchInfo::PlayerMatchData_t *pPlayerData = pMatch->GetMatchDataForPlayer( i );
+ if ( !pPlayerData->bDropped && GetGameTeamForGCTeam( pPlayerData->eGCTeam ) == iTeam )
+ {
+ CBasePlayer *pPlayer = UTIL_PlayerBySteamID( pPlayerData->steamID );
+ // XXX(JohnS): Not quite valid yet, We let them join first onto spectate, which is probably a bit
+ // confusing
+ //
+ // AssertMsg( !pPlayer || ToTFPlayer( pPlayer )->GetTeamNumber() == GetGameTeamForGCTeam( pPlayerData->eGCTeam ),
+ // "Player's GC assigned team does not match their current team" );
+ if ( !pPlayer || !m_bPlayerReady[ pPlayer->entindex() ] )
+ return false;
+
+ iPlayerReadyCount++;
+ }
+ }
+
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc && pMatchDesc->m_params.m_bAutoReady )
+ {
+ return iPlayerReadyCount > 0 || pMatch->GetNumTotalMatchPlayers() == 1 ;
+ }
+ else
+ {
+ int iTeamSize = IsMannVsMachineMode() ? pMatch->GetCanonicalMatchSize() : pMatch->GetCanonicalMatchSize() / 2;
+ return iPlayerReadyCount >= iTeamSize;
+ }
+ }
+
+ // Non-match
+ bool bAtLeastOneReady = false;
+ for ( int i = 1; i <= MAX_PLAYERS; ++i )
+ {
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
+ if ( !pPlayer || ToTFPlayer( pPlayer )->GetTeamNumber() != iTeam )
+ continue;
+
+ if ( !m_bPlayerReady[i] )
+ {
+ return false;
+ }
+ else
+ {
+ bAtLeastOneReady = true;
+ }
+ }
+
+ // Team isn't ready if there was nobody on it.
+ return bAtLeastOneReady;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::PlayerReadyStatus_ShouldStartCountdown( void )
+{
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+
+#if defined( STAGING_ONLY )
+ // Local testing hack - allow match size of one where just that player is ready
+ if ( !IsMannVsMachineMode() && !pMatch && BHavePlayers() && ( IsTeamReady( TF_TEAM_RED ) || IsTeamReady( TF_TEAM_BLUE ) ) )
+ return true;
+#endif // STAGING_ONLY
+
+ if ( IsMannVsMachineMode() )
+ {
+ if ( !IsTeamReady( TF_TEAM_PVE_DEFENDERS ) && m_flRestartRoundTime >= gpGlobals->curtime + mp_tournament_readymode_countdown.GetInt() )
+ {
+ bool bIsTeamReady = PlayerReadyStatus_ArePlayersOnTeamReady( TF_TEAM_PVE_DEFENDERS );
+ if ( bIsTeamReady )
+ {
+ SetTeamReadyState( true, TF_TEAM_PVE_DEFENDERS );
+ return true;
+ }
+ }
+ }
+ else if ( pMatch &&
+ PlayerReadyStatus_ArePlayersOnTeamReady( TF_TEAM_RED ) &&
+ PlayerReadyStatus_ArePlayersOnTeamReady( TF_TEAM_BLUE ) )
+ {
+ return true;
+ }
+ else if ( IsTeamReady( TF_TEAM_RED ) && IsTeamReady( TF_TEAM_BLUE ) )
+ {
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::PlayerReadyStatus_ResetState( void )
+{
+ // Reset the players
+ ResetPlayerAndTeamReadyState();
+
+ // Reset the team
+ SetTeamReadyState( false, TF_TEAM_RED );
+ SetTeamReadyState( false, TF_TEAM_BLUE );
+
+ m_flRestartRoundTime.Set( -1.f );
+ mp_restartgame.SetValue( 0 );
+ m_bAwaitingReadyRestart = true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::PlayerReadyStatus_UpdatePlayerState( CTFPlayer *pTFPlayer, bool bState )
+{
+ if ( !UsePlayerReadyStatusMode() )
+ return;
+
+ if ( !pTFPlayer )
+ return;
+
+ if ( pTFPlayer->GetTeamNumber() < FIRST_GAME_TEAM )
+ return;
+
+ if ( State_Get() != GR_STATE_BETWEEN_RNDS )
+ return;
+
+ // Don't allow toggling state in the final countdown
+ if ( GetRoundRestartTime() > 0.f && GetRoundRestartTime() <= gpGlobals->curtime + TOURNAMENT_NOCANCEL_TIME )
+ return;
+
+ // Make sure we have enough to allow ready mode commands
+ if ( !PlayerReadyStatus_HaveMinPlayersToEnable() )
+ return;
+
+ int nEntIndex = pTFPlayer->entindex();
+
+ // Already this state
+ if ( bState == IsPlayerReady( nEntIndex ) )
+ return;
+
+ SetPlayerReadyState( nEntIndex, bState );
+
+ if ( !bState )
+ {
+ // Slam team status to Not Ready for any player that sets Not Ready
+ m_bTeamReady.Set( pTFPlayer->GetTeamNumber(), false );
+
+ // If everyone cancels ready state, stop the clock
+ bool bAnyoneReady = false;
+ for ( int i = 1; i <= MAX_PLAYERS; ++i )
+ {
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
+ if ( !pPlayer )
+ continue;
+
+ if ( m_bPlayerReady[i] )
+ {
+ bAnyoneReady = true;
+ break;
+ }
+ }
+
+ if ( !bAnyoneReady )
+ {
+ m_flRestartRoundTime.Set( -1.f );
+ mp_restartgame.SetValue( 0 );
+ ResetPlayerAndTeamReadyState();
+ }
+ }
+ else
+ {
+ if ( IsMannVsMachineMode() || IsCompetitiveMode() )
+ {
+ // Reduce timer as each player hits Ready, but only once per-player
+ if ( !m_bPlayerReadyBefore[nEntIndex] && m_flRestartRoundTime > gpGlobals->curtime + 60.f )
+ {
+ float flReduceBy = 30.f;
+ if ( m_flRestartRoundTime < gpGlobals->curtime + 90.f )
+ {
+ // Never reduce below 60 seconds remaining
+ flReduceBy = m_flRestartRoundTime - gpGlobals->curtime - 60.f;
+ }
+
+ m_flRestartRoundTime -= flReduceBy;
+ }
+ else if ( m_flRestartRoundTime < 0 && !PlayerReadyStatus_ShouldStartCountdown() )
+ {
+ m_flRestartRoundTime.Set( gpGlobals->curtime + 150.f );
+ m_bAwaitingReadyRestart = false;
+
+ IGameEvent* pEvent = gameeventmanager->CreateEvent( "teamplay_round_restart_seconds" );
+ if ( pEvent )
+ {
+ pEvent->SetInt( "seconds", 150 );
+ gameeventmanager->FireEvent( pEvent );
+ }
+ }
+ }
+
+ // Unofficial modes set team ready state here
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ if ( !pMatch && !IsMannVsMachineMode() )
+ {
+ int nRed = 0;
+ int nRedCount = 0;
+ int nBlue = 0;
+ int nBlueCount = 0;
+
+ for ( int iTeam = FIRST_GAME_TEAM; iTeam < TFTeamMgr()->GetTeamCount(); iTeam++ )
+ {
+ CTFTeam *pTeam = GetGlobalTFTeam( iTeam );
+ if ( pTeam )
+ {
+ Assert( pTeam->GetTeamNumber() == TF_TEAM_RED || pTeam->GetTeamNumber() == TF_TEAM_BLUE );
+
+ for ( int i = 0; i < pTeam->GetNumPlayers(); ++i )
+ {
+ if ( !pTeam->GetPlayer(i) )
+ continue;
+
+ if ( pTeam->GetTeamNumber() == TF_TEAM_RED && IsPlayerReady( pTeam->GetPlayer(i)->entindex() ) )
+ {
+ if ( !nRedCount )
+ {
+ nRedCount = pTeam->GetNumPlayers();
+ }
+
+ nRed++;
+ }
+ else if ( pTeam->GetTeamNumber() == TF_TEAM_BLUE && IsPlayerReady( pTeam->GetPlayer(i)->entindex() ) )
+ {
+ if ( !nBlueCount )
+ {
+ nBlueCount = pTeam->GetNumPlayers();
+ }
+
+ nBlue++;
+ }
+ }
+ }
+ }
+
+ // Check for the convar that requires min team size, or just go with whatever each team has
+ int nRedMin = ( mp_tournament_readymode_team_size.GetInt() > 0 ) ? mp_tournament_readymode_team_size.GetInt() : Max( nRedCount, 1 );
+ int nBlueMin = ( mp_tournament_readymode_team_size.GetInt() > 0 ) ? mp_tournament_readymode_team_size.GetInt() : Max( nBlueCount, 1 );
+
+ SetTeamReadyState( ( nRed == nRedMin ), TF_TEAM_RED );
+ SetTeamReadyState( ( nBlue == nBlueMin ), TF_TEAM_BLUE );
+ }
+
+ m_bPlayerReadyBefore[nEntIndex] = true;
+ }
+}
+#endif // GAME_DLL
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsDefaultGameMode( void )
+{
+ if ( IsMannVsMachineMode() )
+ return false;
+
+ if ( IsInArenaMode() )
+ return false;
+
+ if ( IsInMedievalMode() )
+ return false;
+
+ if ( IsCompetitiveMode() )
+ return false;
+
+ if ( IsInTournamentMode() )
+ return false;
+
+ if ( IsInTraining() )
+ return false;
+
+ if ( IsInItemTestingMode() )
+ return false;
+
+#ifdef STAGING_ONLY
+ if ( IsPVEModeActive() )
+ return false;
+#endif // STAGING_ONLY
+
+#ifdef TF_RAID_MODE
+ if ( IsRaidMode() )
+ return false;
+
+ if ( IsBossBattleMode() )
+ return false;
+#endif // TF_RAID_MODE
+
+#ifdef TF_CREEP_MODE
+ if ( IsCreepWaveMode() )
+ return false;
+#endif // TF_CREEP_MODE
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Allows us to give situational discounts, such as secondary weapons
+//-----------------------------------------------------------------------------
+int CTFGameRules::GetCostForUpgrade( CMannVsMachineUpgrades *pUpgrade, int iItemSlot, int nClass, CTFPlayer *pPurchaser /*= NULL*/ )
+{
+ Assert( pUpgrade );
+
+ if ( !pUpgrade )
+ return 0;
+
+ int iCost = pUpgrade->nCost;
+
+ CEconItemSchema *pSchema = ItemSystem()->GetItemSchema();
+ if ( pSchema )
+ {
+ const CEconItemAttributeDefinition *pAttr = pSchema->GetAttributeDefinitionByName( pUpgrade->szAttrib );
+ if ( pAttr )
+ {
+ if ( ( iItemSlot == LOADOUT_POSITION_PRIMARY && nClass == TF_CLASS_ENGINEER ) ||
+ ( iItemSlot == LOADOUT_POSITION_SECONDARY && nClass != TF_CLASS_DEMOMAN ) ||
+ ( iItemSlot == LOADOUT_POSITION_MELEE && nClass != TF_CLASS_SPY && nClass != TF_CLASS_ENGINEER ) )
+ {
+ switch ( pAttr->GetDefinitionIndex() )
+ {
+ case 4: // clip size bonus
+ case 6: // fire rate bonus
+ case 78: // maxammo secondary increased
+ case 180: // heal on kill
+ case 266: // projectile penetration
+ case 335: // clip size bonus upgrade
+ case 397: // projectile penetration heavy
+ iCost *= 0.5f;
+ break;
+ default:
+ break;
+ }
+ }
+ else if ( iItemSlot == LOADOUT_POSITION_ACTION )
+ {
+ if ( pPurchaser )
+ {
+ int iCanteenSpec = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pPurchaser, iCanteenSpec, canteen_specialist );
+ if ( iCanteenSpec )
+ {
+
+// iCost *= ( 1.f - ( iCanteenSpec * 0.1f ) );
+// iCost = Max( 1, ( iCost - ( iCost % 5 ) ) );
+
+ iCost -= ( 10 * iCanteenSpec );
+ iCost = Max( 5, iCost );
+ }
+ }
+ }
+ }
+ }
+
+ return iCost;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+CTFGameRules::CTFGameRules()
+#ifdef GAME_DLL
+ : m_mapCoachToStudentMap( DefLessFunc(uint32) )
+ , m_flNextStrangeEventProcessTime( g_flStrangeEventBatchProcessInterval )
+ , m_mapTeleportLocations( DefLessFunc(string_t) )
+ , m_bMapCycleNeedsUpdate( false )
+ , m_flSafeToLeaveTimer( -1.f )
+ , m_flCompModeRespawnPlayersAtMatchStart( -1.f )
+ , m_bMapForcedTruceDuringBossFight( false )
+ , m_flNextHalloweenGiftUpdateTime( -1 )
+#else
+ : m_bRecievedBaseline( false )
+#endif
+{
+#ifdef GAME_DLL
+ // Create teams.
+ TFTeamMgr()->Init();
+
+ ResetMapTime();
+
+ // Create the team managers
+// for ( int i = 0; i < ARRAYSIZE( teamnames ); i++ )
+// {
+// CTeam *pTeam = static_cast<CTeam*>(CreateEntityByName( "tf_team" ));
+// pTeam->Init( sTeamNames[i], i );
+//
+// g_Teams.AddToTail( pTeam );
+// }
+
+ m_flIntermissionEndTime = 0.0f;
+ m_flNextPeriodicThink = 0.0f;
+
+ ListenForGameEvent( "teamplay_point_captured" );
+ ListenForGameEvent( "teamplay_capture_blocked" );
+ ListenForGameEvent( "teamplay_round_win" );
+ ListenForGameEvent( "teamplay_flag_event" );
+ ListenForGameEvent( "teamplay_round_start" );
+ ListenForGameEvent( "player_escort_score" );
+ ListenForGameEvent( "player_disconnect" );
+ ListenForGameEvent( "teamplay_setup_finished" );
+ ListenForGameEvent( "recalculate_truce" );
+
+ Q_memset( m_vecPlayerPositions,0, sizeof(m_vecPlayerPositions) );
+
+ m_iPrevRoundState = -1;
+ m_iCurrentRoundState = -1;
+ m_iCurrentMiniRoundMask = 0;
+
+ m_iPreviousTeamSize = 0;
+
+ // Lets execute a map specific cfg file
+ // ** execute this after server.cfg!
+ char szCommand[MAX_PATH] = { 0 };
+ // Map names cannot contain quotes or control characters so this is safe but silly that we have to do it.
+ Q_snprintf( szCommand, sizeof( szCommand ), "exec \"%s.cfg\"\n", STRING( gpGlobals->mapname ) );
+ engine->ServerCommand( szCommand );
+
+ m_flSendNotificationTime = 0.0f;
+
+ m_bOvertimeAllowedForCTF = true;
+
+ m_redPayloadToPush = NULL;
+ m_bluePayloadToPush = NULL;
+ m_redPayloadToBlock = NULL;
+ m_bluePayloadToBlock = NULL;
+
+ SetCTFCaptureBonusTime( -1 );
+
+ m_hasSpawnedToy = false;
+
+ m_flCapInProgressBuffer = 0.f;
+
+ m_bMannVsMachineAlarmStatus = false;
+ m_flNextFlagAlarm = 0.0f;
+ m_flNextFlagAlert = 0.0f;
+
+ m_doomsdaySetupTimer.Invalidate();
+ StopDoomsdayTicketsTimer();
+
+ m_nPowerupKillsRedTeam = 0;
+ m_nPowerupKillsBlueTeam = 0;
+ m_flTimeToRunImbalanceMeasures = 120.f;
+ m_flTimeToStopImbalanceMeasures = 0.f;
+ m_bPowerupImbalanceMeasuresRunning = false;
+
+ m_hRequiredObserverTarget = NULL;
+ m_bStopWatchWinner.Set( false );
+
+#else // GAME_DLL
+
+ // Vision Filter Translations for swapping out particle effects and models
+ SetUpVisionFilterKeyValues();
+
+ m_bSillyGibs = CommandLine()->FindParm( "-sillygibs" ) ? true : false;
+ if ( m_bSillyGibs )
+ {
+ cl_burninggibs.SetValue( 0 );
+ }
+ // @todo Tom Bui: game_newmap doesn't seem to be used...
+ ListenForGameEvent( "game_newmap" );
+ ListenForGameEvent( "overtime_nag" );
+ ListenForGameEvent( "recalculate_holidays" );
+
+#endif
+
+ ClearHalloweenEffectStatus();
+
+ // Initialize the game type
+ m_nGameType.Set( TF_GAMETYPE_UNDEFINED );
+
+ m_bPlayingMannVsMachine.Set( false );
+ m_bMannVsMachineAlarmStatus.Set( false );
+ m_bBountyModeEnabled.Set( false );
+
+ m_bPlayingKoth.Set( false );
+ m_bPlayingMedieval.Set( false );
+ m_bPlayingHybrid_CTF_CP.Set( false );
+ m_bPlayingSpecialDeliveryMode.Set( false );
+ m_bPlayingRobotDestructionMode.Set( false );
+ m_bPowerupMode.Set( false );
+
+ m_bHelltowerPlayersInHell.Set( false );
+ m_bIsUsingSpells.Set( false );
+ m_bTruceActive.Set( false );
+ m_bTeamsSwitched.Set( false );
+
+ m_halloweenScenario.Set( HALLOWEEN_SCENARIO_NONE );
+ m_iGlobalAttributeCacheVersion = 0;
+
+//=============================================================================
+// HPE_BEGIN
+// [msmith] HUD type
+//=============================================================================
+ m_nHudType.Set( TF_HUDTYPE_UNDEFINED );
+ m_bIsInTraining.Set( false );
+ m_bAllowTrainingAchievements.Set( false );
+ m_bIsWaitingForTrainingContinue.Set( false );
+//=============================================================================
+// HPE_END
+//=============================================================================
+ m_bIsTrainingHUDVisible.Set( false );
+
+ m_bIsInItemTestingMode.Set( false );
+
+ // Set turbo physics on. Do it here for now.
+ sv_turbophysics.SetValue( 1 );
+
+ // Initialize the team manager here, etc...
+
+ // If you hit these asserts its because you added or removed a weapon type
+ // and didn't also add or remove the weapon name or damage type from the
+ // arrays defined in tf_shareddefs.cpp
+ COMPILE_TIME_ASSERT( TF_WEAPON_COUNT == ARRAYSIZE( g_aWeaponDamageTypes ) );
+ COMPILE_TIME_ASSERT( TF_WEAPON_COUNT == ARRAYSIZE( g_aWeaponNames ) );
+
+ m_iPreviousRoundWinners = TEAM_UNASSIGNED;
+
+ m_pszTeamGoalStringRed.GetForModify()[0] = '\0';
+ m_pszTeamGoalStringBlue.GetForModify()[0] = '\0';
+
+ m_nStopWatchState.Set( STOPWATCH_CAPTURE_TIME_NOT_SET );
+
+ mp_tournament_redteamname.Revert();
+ mp_tournament_blueteamname.Revert();
+
+ m_flCapturePointEnableTime = 0.0f;
+
+ m_itHandle = 0;
+
+ m_hBirthdayPlayer = NULL;
+
+ m_nBossHealth = 0;
+ m_nMaxBossHealth = 0;
+ m_fBossNormalizedTravelDistance = 0.0f;
+
+ m_areHealthAndAmmoVectorsReady = false;
+
+ m_flGravityMultiplier.Set( 1.0 );
+
+ m_pszCustomUpgradesFile.GetForModify()[0] = '\0';
+
+ m_bShowMatchSummary.Set( false );
+ m_bMapHasMatchSummaryStage.Set( false );
+ m_bPlayersAreOnMatchSummaryStage.Set( false );
+
+ m_bUseMatchHUD = false;
+ m_bUsePreRoundDoors = false;
+
+ m_nMatchGroupType.Set( k_nMatchGroup_Invalid );
+ m_bMatchEnded.Set( true );
+
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ m_ePlayerWantsRematch.Set( i, USER_NEXT_MAP_VOTE_UNDECIDED );
+ }
+
+ m_eRematchState = NEXT_MAP_VOTE_STATE_NONE;
+
+#ifdef GAME_DLL
+
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ if ( pMatch )
+ {
+ SyncMatchSettings();
+ }
+
+ for ( int i = 0; i < TF_TEAM_COUNT; i++ )
+ {
+ m_bHasSpawnedSoccerBall[i] = false;
+ }
+
+ m_flCheckPlayersConnectingTime = 0;
+
+ m_pUpgrades = NULL;
+
+ // Determine if a halloween event map is active
+ // Map active always turns on Halloween
+ {
+ char szCurrentMap[MAX_MAP_NAME];
+ Q_strncpy( szCurrentMap, STRING( gpGlobals->mapname ), sizeof( szCurrentMap ) );
+
+ if ( !Q_stricmp( szCurrentMap, "cp_manor_event" ) )
+ {
+ m_halloweenScenario.Set( HALLOWEEN_SCENARIO_MANN_MANOR );
+ }
+ else if ( !Q_stricmp( szCurrentMap, "koth_viaduct_event" ) )
+ {
+ m_halloweenScenario.Set( HALLOWEEN_SCENARIO_VIADUCT );
+ }
+ else if ( !Q_stricmp( szCurrentMap, "koth_lakeside_event" ) )
+ {
+ m_halloweenScenario.Set( HALLOWEEN_SCENARIO_LAKESIDE );
+ }
+ else if( !Q_stricmp( szCurrentMap, "plr_hightower_event" ) )
+ {
+ m_halloweenScenario.Set( HALLOWEEN_SCENARIO_HIGHTOWER );
+ }
+ else if ( !Q_stricmp( szCurrentMap, "sd_doomsday_event" ) )
+ {
+ m_halloweenScenario.Set( HALLOWEEN_SCENARIO_DOOMSDAY );
+ }
+ }
+
+ // Our hook for LoadMapCycleFile wont run during the base class constructor that does this initially
+ TrackWorkshopMapsInMapCycle();
+#endif
+}
+
+#ifdef GAME_DLL
+void CTFGameRules::Precache( void )
+{
+ BaseClass::Precache();
+
+ // The Halloween bosses get spawned in code, so they don't get a chance to precache
+ // when the map loads. We'll do the precaching for them here.
+ if( IsHalloweenScenario( HALLOWEEN_SCENARIO_LAKESIDE ) )
+ {
+ CMerasmus::PrecacheMerasmus();
+ }
+ else if( IsHalloweenScenario( HALLOWEEN_SCENARIO_VIADUCT ) )
+ {
+ CEyeballBoss::PrecacheEyeballBoss();
+ }
+ else if( IsHalloweenScenario( HALLOWEEN_SCENARIO_MANN_MANOR ) )
+ {
+ CHeadlessHatman::PrecacheHeadlessHatman();
+ }
+ else if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ CEyeballBoss::PrecacheEyeballBoss();
+ CGhost::PrecacheGhost();
+ }
+ else if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_DOOMSDAY ) )
+ {
+ CTFPlayer::PrecacheKart();
+ CGhost::PrecacheGhost();
+ CHeadlessHatman::PrecacheHeadlessHatman();
+ CMerasmus::PrecacheMerasmus();
+ }
+
+ if ( StringHasPrefix( STRING( gpGlobals->mapname ), "mvm_" ) )
+ {
+ CTFPlayer::PrecacheMvM();
+ }
+
+ CTFPlayer::m_bTFPlayerNeedsPrecache = true;
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+//#ifdef GAME_DLL
+//extern void AddHalloweenGiftPositionsForMap( const char *pszMapName, CUtlVector<Vector> &vLocations );
+//#endif
+
+void CTFGameRules::LevelInitPostEntity( void )
+{
+ BaseClass::LevelInitPostEntity();
+
+#ifdef GAME_DLL
+ // Refind our proxy, because we might have had it deleted due to a mapmaker placed one
+ m_hGamerulesProxy = dynamic_cast<CTFGameRulesProxy*>( gEntList.FindEntityByClassname( NULL, "tf_gamerules" ) );
+
+ // Halloween
+ // Flush Halloween Gift Location and grab locations if applicable. NonHalloween maps will have these as zero
+ m_halloweenGiftSpawnLocations.Purge();
+
+ if ( IsHolidayActive( kHoliday_Halloween ) )
+ {
+ for ( int i=0; i<IHalloweenGiftSpawnAutoList::AutoList().Count(); ++i )
+ {
+ CHalloweenGiftSpawnLocation* pGift = static_cast< CHalloweenGiftSpawnLocation* >( IHalloweenGiftSpawnAutoList::AutoList()[i] );
+ m_halloweenGiftSpawnLocations.AddToTail( pGift->GetAbsOrigin() );
+ UTIL_Remove( pGift );
+ }
+
+ // Ask Halloween System if there are any locations
+ AddHalloweenGiftPositionsForMap( STRING(gpGlobals->mapname), m_halloweenGiftSpawnLocations );
+ }
+
+ m_flMatchSummaryTeleportTime = -1.f;
+
+ //tagES
+#ifdef STAGING_ONLY
+ tf_test_match_summary.SetValue( 0 );
+#endif
+
+ const IMatchGroupDescription *pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc )
+ {
+ pMatchDesc->InitGameRulesSettingsPostEntity();
+ }
+
+#endif // GAME_DLL
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+float CTFGameRules::GetRespawnTimeScalar( int iTeam )
+{
+ // In PvE mode, we don't modify respawn times
+ if ( IsPVEModeActive() )
+ return 1.0;
+
+ return BaseClass::GetRespawnTimeScalar( iTeam );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+float CTFGameRules::GetRespawnWaveMaxLength( int iTeam, bool bScaleWithNumPlayers )
+{
+ bool bScale = bScaleWithNumPlayers;
+
+#ifdef TF_RAID_MODE
+ if ( IsRaidMode() )
+ {
+ return tf_raid_respawn_time.GetFloat();
+ }
+#endif // TF_RAID_MODE
+
+#ifdef TF_CREEP_MODE
+ if ( IsCreepWaveMode() )
+ {
+ return tf_creep_wave_player_respawn_time.GetFloat();
+ }
+#endif
+
+ if ( IsMannVsMachineMode() )
+ {
+ bScale = false;
+ }
+
+ float flTime = BaseClass::GetRespawnWaveMaxLength( iTeam, bScale );
+
+ CTFRobotDestructionLogic* pRoboLogic = CTFRobotDestructionLogic::GetRobotDestructionLogic();
+ if ( pRoboLogic )
+ {
+ flTime *= ( 1.f - pRoboLogic->GetRespawnScaleForTeam( iTeam ) );
+ }
+
+ return flTime;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::FlagsMayBeCapped( void )
+{
+ if ( State_Get() != GR_STATE_TEAM_WIN && State_Get() != GR_STATE_PREROUND )
+ return true;
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Return which Halloween scenario is currently running
+//-----------------------------------------------------------------------------
+CTFGameRules::HalloweenScenarioType CTFGameRules::GetHalloweenScenario( void ) const
+{
+ if ( !const_cast< CTFGameRules * >( this )->IsHolidayActive( kHoliday_Halloween ) )
+ return HALLOWEEN_SCENARIO_NONE;
+
+ return m_halloweenScenario;
+}
+
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsUsingSpells( void ) const
+{
+ if ( tf_spells_enabled.GetBool() )
+ return true;
+
+ if ( IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ return true;
+
+ return m_bIsUsingSpells;
+}
+
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsUsingGrapplingHook( void ) const
+{
+ return tf_grapplinghook_enable.GetBool();
+}
+
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsTruceActive( void ) const
+{
+ return m_bTruceActive;
+}
+
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CanInitiateDuels( void )
+{
+ if ( IsInWaitingForPlayers() )
+ return false;
+
+ gamerules_roundstate_t roundState = State_Get();
+ if ( ( roundState != GR_STATE_RND_RUNNING ) && ( roundState != GR_STATE_PREROUND ) )
+ return false;
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CTFGameRules::GetGameTeamForGCTeam( TF_GC_TEAM nGCTeam )
+{
+ if ( nGCTeam == TF_GC_TEAM_INVADERS )
+ {
+ if ( IsCompetitiveMode() )
+ {
+ return ( m_bTeamsSwitched ) ? TF_TEAM_RED : TF_TEAM_BLUE;
+ }
+
+ return TF_TEAM_BLUE;
+ }
+ else if ( nGCTeam == TF_GC_TEAM_DEFENDERS )
+ {
+ if ( IsCompetitiveMode() )
+ {
+ return ( m_bTeamsSwitched ) ? TF_TEAM_BLUE : TF_TEAM_RED;
+ }
+
+ return TF_TEAM_RED;
+ }
+
+ return TEAM_UNASSIGNED;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+TF_GC_TEAM CTFGameRules::GetGCTeamForGameTeam( int nGameTeam )
+{
+ if ( nGameTeam == TF_TEAM_BLUE )
+ {
+ if ( IsCompetitiveMode() )
+ {
+ return ( m_bTeamsSwitched ) ? TF_GC_TEAM_DEFENDERS : TF_GC_TEAM_INVADERS;
+ }
+
+ return TF_GC_TEAM_INVADERS;
+ }
+ else if ( nGameTeam == TF_TEAM_RED )
+ {
+ if ( IsCompetitiveMode() )
+ {
+ return ( m_bTeamsSwitched ) ? TF_GC_TEAM_INVADERS : TF_GC_TEAM_DEFENDERS;
+ }
+
+ return TF_GC_TEAM_DEFENDERS;
+ }
+
+ return TF_GC_TEAM_NOTEAM;
+}
+
+CTFGameRules::EUserNextMapVote CTFGameRules::GetWinningVote( int (&nVotes)[ EUserNextMapVote::NUM_VOTE_STATES ] ) const
+{
+ // We assume "undecided" is the index just after the last vote option
+ COMPILE_TIME_ASSERT( USER_NEXT_MAP_VOTE_UNDECIDED == NEXT_MAP_VOTE_OPTIONS );
+ memset( nVotes, 0, sizeof( nVotes ) );
+ int nTotalPlayers = 0;
+
+ // Tally up votes.
+ for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ )
+ {
+#ifdef CLIENT_DLL
+
+ if ( !g_PR || !g_PR->IsConnected( iPlayerIndex ) )
+ continue;
+
+#else // GAME_DLL
+ // We care about those that are still here. If you leave, you don't count towards the vote total
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+ if ( !pTFPlayer || pTFPlayer->IsBot() )
+ continue;
+
+
+ if ( !pTFPlayer->IsConnected() )
+ continue;
+
+ CSteamID steamID;
+ pTFPlayer->GetSteamID( &steamID );
+
+ // People without parties *should* be getting a new one soon. Count them as undecided
+ // until their party shows up and they're allowed to make a real vote.
+ CTFParty* pParty = GTFGCClientSystem()->GetPartyForPlayer( steamID );
+ if ( !pParty )
+ {
+ ++nVotes[ EUserNextMapVote::USER_NEXT_MAP_VOTE_UNDECIDED ];
+ ++nTotalPlayers;
+ continue;
+ }
+
+ const CMatchInfo* pMatch = GTFGCClientSystem()->GetMatch();
+ Assert( pMatch );
+ if ( !pMatch )
+ {
+ continue;
+ }
+
+ // Need to be a match players
+ const CMatchInfo::PlayerMatchData_t* pPlayerMatchData = pMatch->GetMatchDataForPlayer( steamID );
+ Assert( pPlayerMatchData );
+ if ( !pPlayerMatchData )
+ {
+ // How'd you get here?
+ continue;
+ }
+#endif
+
+ nTotalPlayers++;
+ nVotes[ TFGameRules()->PlayerNextMapVoteState( iPlayerIndex ) ]++;
+ }
+
+ if ( nVotes[ USER_NEXT_MAP_VOTE_UNDECIDED ] == nTotalPlayers )
+ {
+ return USER_NEXT_MAP_VOTE_UNDECIDED;
+ }
+ else
+ {
+ EUserNextMapVote eWinningVote = USER_NEXT_MAP_VOTE_MAP_0;
+
+ for( int i = 0; i < NEXT_MAP_VOTE_OPTIONS; ++i )
+ {
+ // The current map is in slot 0. >= so we favor change.
+ eWinningVote = nVotes[ i ] >= nVotes[ eWinningVote ]
+ ? (EUserNextMapVote)i
+ : eWinningVote;
+ }
+
+ return eWinningVote;
+ }
+}
+
+#ifdef GAME_DLL
+void CTFGameRules::UpdateNextMapVoteOptionsFromLobby()
+{
+ for( int i = 0; i < NEXT_MAP_VOTE_OPTIONS; ++i )
+ {
+ m_nNextMapVoteOptions.Set( i, GTFGCClientSystem()->GetNextMapVoteByIndex( i )->m_nDefIndex );
+ }
+}
+
+void CTFGameRules::KickPlayersNewMatchIDRequestFailed()
+{
+ Assert( m_eRematchState == NEXT_MAP_VOTE_STATE_MAP_CHOSEN_PAUSE );
+
+ // Let everyone know the rematch failed.
+ if ( m_eRematchState == NEXT_MAP_VOTE_STATE_MAP_CHOSEN_PAUSE )
+ {
+ CBroadcastRecipientFilter filter;
+ UTIL_ClientPrintFilter( filter, HUD_PRINTTALK, "#TF_Matchmaking_RollingQueue_NewRematch_GCFail" );
+
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "rematch_failed_to_create" );
+ if ( pEvent )
+ {
+ gameeventmanager->FireEvent( pEvent );
+ }
+ }
+
+ // The GC failed to get a new MatchID for us. Let's clear out and reset.
+ engine->ServerCommand( "kickall #TF_Competitive_Disconnect\n" );
+
+ // Tell the GC System to end the managed match mode -- we skipped this in StopCompetitiveMatch so we could roll the
+ // managed match into a new one.
+ Assert( !IsManagedMatchEnded() );
+ if ( !IsManagedMatchEnded() )
+ {
+ GTFGCClientSystem()->EndManagedMatch( /* bKickPlayersToParties */ false );
+ Assert( IsManagedMatchEnded() );
+ m_bMatchEnded.Set( true );
+ }
+
+ // Prepare for next match
+ g_fGameOver = false;
+ m_bAllowBetweenRounds = true;
+ State_Transition( GR_STATE_RESTART );
+ SetInWaitingForPlayers( true );
+}
+
+//-----------------------------------------------------------------------------
+void CTFGameRules::CheckAndSetPartyLeader( CTFPlayer *pTFPlayer, int iTeam )
+{
+ if ( !pTFPlayer )
+ return;
+
+ Assert( iTeam >= FIRST_GAME_TEAM );
+ if ( iTeam < FIRST_GAME_TEAM )
+ return;
+
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ if ( !pMatch )
+ return;
+
+ CSteamID steamID;
+ if ( !pTFPlayer->GetSteamID( &steamID ) )
+ return;
+
+ // TODO: Whenever a lobby is updated, look at the CTFLobbyMembers and see if
+ // everyone has the same partyID and then set whoever is the leader to
+ // have their name be the team name
+
+ //CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( steamID );
+ //if ( pMatchPlayer && pMatchPlayer->bPremadeLeader )
+ //{
+ // CTFPlayerResource *pResource = dynamic_cast< CTFPlayerResource* >( g_pPlayerResource );
+ // if ( pResource )
+ // {
+ // pResource->SetPartyLeaderIndex( iTeam, pTFPlayer->entindex() );
+ // }
+ //}
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Sets current boss victim
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetIT( CBaseEntity *who )
+{
+ if ( IsHolidayActive( kHoliday_Halloween ) && !IsHalloweenScenario( HALLOWEEN_SCENARIO_DOOMSDAY ) )
+ {
+ CTFPlayer* newIT = ToTFPlayer( who );
+
+ if ( newIT && newIT != m_itHandle.Get() )
+ {
+ // new IT victim - warn them
+ ClientPrint( newIT, HUD_PRINTTALK, "#TF_HALLOWEEN_BOSS_WARN_VICTIM", newIT->GetPlayerName() );
+ ClientPrint( newIT, HUD_PRINTCENTER, "#TF_HALLOWEEN_BOSS_WARN_VICTIM", newIT->GetPlayerName() );
+
+ CSingleUserReliableRecipientFilter filter( newIT );
+ newIT->EmitSound( filter, newIT->entindex(), "Player.YouAreIT" );
+
+ // force them to scream when they become it
+ newIT->EmitSound( "Halloween.PlayerScream" );
+ }
+
+ CTFPlayer *oldIT = ToTFPlayer( m_itHandle );
+
+ if ( oldIT && oldIT != who && oldIT->IsAlive() )
+ {
+ // tell old IT player they are safe
+ CSingleUserReliableRecipientFilter filter( oldIT );
+ oldIT->EmitSound( filter, oldIT->entindex(), "Player.TaggedOtherIT" );
+
+ ClientPrint( oldIT, HUD_PRINTTALK, "#TF_HALLOWEEN_BOSS_LOST_AGGRO" );
+ ClientPrint( oldIT, HUD_PRINTCENTER, "#TF_HALLOWEEN_BOSS_LOST_AGGRO" );
+ }
+ }
+
+ m_itHandle = who;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Sets current birthday player
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetBirthdayPlayer( CBaseEntity *pEntity )
+{
+/*
+ if ( IsBirthday() )
+ {
+ if ( pEntity && pEntity->IsPlayer() && pEntity != m_hBirthdayPlayer.Get() )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( pEntity );
+ if ( pTFPlayer )
+ {
+ // new IT victim - warn them
+// ClientPrint( pTFPlayer, HUD_PRINTTALK, "#TF_HALLOWEEN_BOSS_WARN_VICTIM", player->GetPlayerName() );
+// ClientPrint( pTFPlayer, HUD_PRINTCENTER, "#TF_HALLOWEEN_BOSS_WARN_VICTIM", player->GetPlayerName() );
+
+ CSingleUserReliableRecipientFilter filter( pTFPlayer );
+ pTFPlayer->EmitSound( filter, pTFPlayer->entindex(), "Game.HappyBirthday" );
+
+ // force them to scream when they become it
+// pTFPlayer->EmitSound( "Halloween.PlayerScream" );
+ }
+ }
+
+// CTFPlayer *oldIT = ToTFPlayer( m_itHandle );
+//
+// if ( oldIT && oldIT != who && oldIT->IsAlive() )
+// {
+// // tell old IT player they are safe
+// CSingleUserReliableRecipientFilter filter( oldIT );
+// oldIT->EmitSound( filter, oldIT->entindex(), "Player.TaggedOtherIT" );
+//
+// ClientPrint( oldIT, HUD_PRINTTALK, "#TF_HALLOWEEN_BOSS_LOST_AGGRO" );
+// ClientPrint( oldIT, HUD_PRINTCENTER, "#TF_HALLOWEEN_BOSS_LOST_AGGRO" );
+// }
+
+ m_hBirthdayPlayer = pEntity;
+ }
+ else
+ {
+ m_hBirthdayPlayer = NULL;
+ }
+*/
+}
+
+
+#ifdef GAME_DLL
+
+//-----------------------------------------------------------------------------
+// Purpose: remove all projectiles in the world
+//-----------------------------------------------------------------------------
+void CTFGameRules::RemoveAllProjectiles()
+{
+ for ( int i=0; i<IBaseProjectileAutoList::AutoList().Count(); ++i )
+ {
+ UTIL_Remove( static_cast< CBaseProjectile* >( IBaseProjectileAutoList::AutoList()[i] ) );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: remove all buildings in the world
+//-----------------------------------------------------------------------------
+void CTFGameRules::RemoveAllBuildings( bool bExplodeBuildings /*= false*/ )
+{
+ for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i )
+ {
+ CBaseObject *pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] );
+ if ( !pObj->IsMapPlaced() )
+ {
+ // 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 )
+ {
+ CTFPlayer *pOwner = pObj->GetOwner();
+ event->SetInt( "userid", pOwner ? pOwner->GetUserID() : -1 ); // user ID of the object owner
+ event->SetInt( "objecttype", pObj->GetType() ); // type of object removed
+ event->SetInt( "index", pObj->entindex() ); // index of the object removed
+ gameeventmanager->FireEvent( event );
+ }
+
+ if ( bExplodeBuildings )
+ {
+ pObj->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
+ pObj->SetSolid( SOLID_NONE );
+ UTIL_Remove( pObj );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: remove all sentries ammo
+//-----------------------------------------------------------------------------
+void CTFGameRules::RemoveAllSentriesAmmo()
+{
+ for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i )
+ {
+ CBaseObject *pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] );
+ if ( pObj->GetType() == OBJ_SENTRYGUN )
+ {
+ CObjectSentrygun *pSentry = assert_cast< CObjectSentrygun* >( pObj );
+ pSentry->RemoveAllAmmo();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Removes all projectiles and buildings from world
+//-----------------------------------------------------------------------------
+void CTFGameRules::RemoveAllProjectilesAndBuildings( bool bExplodeBuildings /*= false*/ )
+{
+ RemoveAllProjectiles();
+ RemoveAllBuildings( bExplodeBuildings );
+}
+#endif // GAME_DLL
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Determines whether we should allow mp_timelimit to trigger a map change
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CanChangelevelBecauseOfTimeLimit( void )
+{
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+
+ // we only want to deny a map change triggered by mp_timelimit if we're not forcing a map reset,
+ // we're playing mini-rounds, and the master says we need to play all of them before changing (for maps like Dustbowl)
+ if ( !m_bForceMapReset && pMaster && pMaster->PlayingMiniRounds() && pMaster->ShouldPlayAllControlPointRounds() )
+ {
+ if ( pMaster->NumPlayableControlPointRounds() > 0 )
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CanGoToStalemate( void )
+{
+ // In CTF, don't go to stalemate if one of the flags isn't at home
+ if ( m_nGameType == TF_GAMETYPE_CTF )
+ {
+ for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i )
+ {
+ CCaptureFlag *pFlag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[i] );
+ if ( pFlag->IsDropped() || pFlag->IsStolen() )
+ return false;
+ }
+
+ // check that one team hasn't won by capping
+ if ( CheckCapsPerRound() )
+ return false;
+ }
+
+ return BaseClass::CanGoToStalemate();
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::RestoreActiveTimer( void )
+{
+ BaseClass::RestoreActiveTimer();
+
+ if ( IsInKothMode() )
+ {
+ CTeamRoundTimer *pTimer = GetBlueKothRoundTimer();
+ if ( pTimer )
+ {
+ pTimer->SetShowInHud( true );
+ }
+
+ pTimer = GetRedKothRoundTimer();
+ if ( pTimer )
+ {
+ pTimer->SetShowInHud( true );
+ }
+ }
+}
+
+// Classnames of entities that are preserved across round restarts
+static const char *s_PreserveEnts[] =
+{
+ "tf_gamerules",
+ "tf_team_manager",
+ "tf_player_manager",
+ "tf_team",
+ "tf_objective_resource",
+ "keyframe_rope",
+ "move_rope",
+ "tf_viewmodel",
+ "tf_logic_training",
+ "tf_logic_training_mode",
+#ifdef TF_RAID_MODE
+ "tf_logic_raid",
+#endif // TF_RAID_MODE
+ "tf_powerup_bottle",
+ "tf_mann_vs_machine_stats",
+ "tf_wearable",
+ "tf_wearable_demoshield",
+ "tf_wearable_robot_arm",
+ "tf_wearable_vm",
+ "tf_logic_bonusround",
+ "vote_controller",
+ "monster_resource",
+ "tf_logic_medieval",
+ "tf_logic_cp_timer",
+ "tf_logic_tower_defense", // legacy
+ "tf_logic_mann_vs_machine",
+ "func_upgradestation",
+ "entity_rocket",
+ "entity_carrier",
+ "entity_sign",
+ "entity_saucer",
+ "tf_halloween_gift_pickup",
+ "tf_logic_competitive",
+ "", // END Marker
+};
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::Activate()
+{
+ m_nGameType.Set( TF_GAMETYPE_UNDEFINED );
+
+ tf_gamemode_arena.SetValue( 0 );
+ tf_gamemode_cp.SetValue( 0 );
+ tf_gamemode_ctf.SetValue( 0 );
+ tf_gamemode_sd.SetValue( 0 );
+ tf_gamemode_payload.SetValue( 0 );
+ tf_gamemode_mvm.SetValue( 0 );
+ tf_gamemode_rd.SetValue( 0 );
+ tf_gamemode_pd.SetValue( 0 );
+ tf_gamemode_tc.SetValue( 0 );
+ tf_beta_content.SetValue( 0 );
+ tf_gamemode_passtime.SetValue( 0 );
+ tf_gamemode_misc.SetValue( 0 );
+
+ tf_bot_count.SetValue( 0 );
+
+#ifdef TF_RAID_MODE
+ tf_gamemode_raid.SetValue( 0 );
+ tf_gamemode_boss_battle.SetValue( 0 );
+#endif
+ m_bPlayingMannVsMachine.Set( false );
+ m_bBountyModeEnabled.Set( false );
+ m_nCurrencyAccumulator = 0;
+ m_iCurrencyPool = 0;
+ m_bMannVsMachineAlarmStatus.Set( false );
+ m_bPlayingKoth.Set( false );
+ m_bPlayingMedieval.Set( false );
+ m_bPlayingHybrid_CTF_CP.Set( false );
+ m_bPlayingSpecialDeliveryMode.Set( false );
+ m_bPlayingRobotDestructionMode.Set( false );
+ m_bPowerupMode.Set( false );
+
+ m_redPayloadToPush = NULL;
+ m_bluePayloadToPush = NULL;
+ m_redPayloadToBlock = NULL;
+ m_bluePayloadToBlock = NULL;
+
+ m_zombieMobTimer.Invalidate();
+ m_zombiesLeftToSpawn = 0;
+
+ m_CPTimerEnts.RemoveAll();
+
+ m_nMapHolidayType.Set( kHoliday_None );
+
+ CArenaLogic *pArenaLogic = dynamic_cast< CArenaLogic * > (gEntList.FindEntityByClassname( NULL, "tf_logic_arena" ) );
+
+ if ( pArenaLogic != NULL )
+ {
+ m_hArenaEntity = pArenaLogic;
+
+ m_nGameType.Set( TF_GAMETYPE_ARENA );
+
+ tf_gamemode_arena.SetValue( 1 );
+
+ Msg( "Executing server arena config file\n" );
+ engine->ServerCommand( "exec config_arena.cfg\n" );
+ }
+
+#ifdef TF_RAID_MODE
+ CRaidLogic *pRaidLogic = dynamic_cast< CRaidLogic * >( gEntList.FindEntityByClassname( NULL, "tf_logic_raid" ) );
+ if ( pRaidLogic )
+ {
+ m_hRaidLogic = pRaidLogic;
+
+ tf_gamemode_raid.SetValue( 1 );
+
+ Msg( "Executing server raid game mode config file\n" );
+ engine->ServerCommand( "exec config_raid.cfg\n" );
+ }
+
+ CBossBattleLogic *pBossBattleLogic = dynamic_cast< CBossBattleLogic * >( gEntList.FindEntityByClassname( NULL, "tf_logic_boss_battle" ) );
+ if ( pBossBattleLogic )
+ {
+ m_hBossBattleLogic = pBossBattleLogic;
+
+ tf_gamemode_boss_battle.SetValue( 1 );
+ }
+#endif // TF_RAID_MODE
+
+ // This is beta content if this map has "beta" as a tag in the schema
+ {
+ const MapDef_t* pMap = GetItemSchema()->GetMasterMapDefByName( STRING( gpGlobals->mapname ) );
+ if ( pMap && pMap->vecTags.HasElement( GetItemSchema()->GetHandleForTag( "beta" ) ) )
+ {
+ tf_beta_content.SetValue( 1 );
+ }
+ }
+
+ if ( !Q_strncmp( STRING( gpGlobals->mapname ), "tc_", 3 ) )
+ {
+ tf_gamemode_tc.SetValue( 1 );
+ }
+
+ CMannVsMachineLogic *pMannVsMachineLogic = dynamic_cast< CMannVsMachineLogic * >( gEntList.FindEntityByClassname( NULL, "tf_logic_mann_vs_machine" ) );
+ CTeamTrainWatcher *pTrainWatch = dynamic_cast<CTeamTrainWatcher*> ( gEntList.FindEntityByClassname( NULL, "team_train_watcher" ) );
+ bool bFlag = ICaptureFlagAutoList::AutoList().Count() > 0;
+ if ( CTFRobotDestructionLogic::GetRobotDestructionLogic() )
+ {
+ m_bPlayingRobotDestructionMode.Set( true );
+ if ( CTFRobotDestructionLogic::GetRobotDestructionLogic()->GetType() == CTFRobotDestructionLogic::TYPE_ROBOT_DESTRUCTION )
+ {
+ tf_gamemode_rd.SetValue( 1 );
+ m_nGameType.Set( TF_GAMETYPE_RD );
+ tf_beta_content.SetValue( 1 );
+ }
+ else
+ {
+ tf_gamemode_pd.SetValue( 1 );
+ m_nGameType.Set( TF_GAMETYPE_PD );
+ }
+ }
+ else if ( pMannVsMachineLogic )
+ {
+ m_bPlayingMannVsMachine.Set( true );
+ tf_gamemode_mvm.SetValue( 1 );
+ m_nGameType.Set( TF_GAMETYPE_MVM );
+ }
+ else if ( StringHasPrefix( STRING( gpGlobals->mapname ), "sd_" ) )
+ {
+ m_bPlayingSpecialDeliveryMode.Set( true );
+ tf_gamemode_sd.SetValue( 1 );
+ }
+ else if ( bFlag && !CTFRobotDestructionLogic::GetRobotDestructionLogic() )
+ {
+ m_nGameType.Set( TF_GAMETYPE_CTF );
+ tf_gamemode_ctf.SetValue( 1 );
+ }
+
+ else if ( pTrainWatch )
+ {
+ m_nGameType.Set( TF_GAMETYPE_ESCORT );
+ tf_gamemode_payload.SetValue( 1 );
+
+ CMultipleEscort *pMultipleEscort = dynamic_cast<CMultipleEscort*> ( gEntList.FindEntityByClassname( NULL, "tf_logic_multiple_escort" ) );
+ SetMultipleTrains( pMultipleEscort != NULL );
+ }
+ else if ( g_hControlPointMasters.Count() && m_nGameType != TF_GAMETYPE_ARENA ) // We have cap points in arena but we're not CP
+ {
+ m_nGameType.Set( TF_GAMETYPE_CP );
+ tf_gamemode_cp.SetValue( 1 );
+ }
+
+ auto *pPasstime = dynamic_cast<CTFPasstimeLogic*> ( gEntList.FindEntityByClassname( NULL, "passtime_logic" ) );
+ if ( pPasstime )
+ {
+ m_nGameType.Set( TF_GAMETYPE_PASSTIME );
+ tf_gamemode_passtime.SetValue( 1 );
+ }
+
+ // the game is in training mode if this entity is found
+ m_hTrainingModeLogic = dynamic_cast< CTrainingModeLogic * > ( gEntList.FindEntityByClassname( NULL, "tf_logic_training_mode" ) );
+ if ( NULL != m_hTrainingModeLogic )
+ {
+ m_bIsInTraining.Set( true );
+ m_bAllowTrainingAchievements.Set( false );
+ mp_humans_must_join_team.SetValue( "blue" );
+ m_bIsTrainingHUDVisible.Set( true );
+ tf_training_client_message.SetValue( (int)TRAINING_CLIENT_MESSAGE_NONE );
+ }
+
+ m_bIsInItemTestingMode.Set( false );
+
+ CKothLogic *pKoth = dynamic_cast<CKothLogic*> ( gEntList.FindEntityByClassname( NULL, "tf_logic_koth" ) );
+ if ( pKoth )
+ {
+ m_bPlayingKoth.Set( true );
+ }
+
+ CMedievalLogic *pMedieval = dynamic_cast<CMedievalLogic*> ( gEntList.FindEntityByClassname( NULL, "tf_logic_medieval" ) );
+ if ( pMedieval || tf_medieval.GetBool() )
+ {
+ m_bPlayingMedieval.Set( true );
+ }
+
+ CCompetitiveLogic *pCompLogic = dynamic_cast< CCompetitiveLogic* > ( gEntList.FindEntityByClassname( NULL, "tf_logic_competitive" ) );
+ if ( pCompLogic )
+ {
+ m_hCompetitiveLogicEntity = pCompLogic;
+ }
+
+ CHybridMap_CTF_CP *pHybridMap_CTF_CP = dynamic_cast<CHybridMap_CTF_CP*> ( gEntList.FindEntityByClassname( NULL, "tf_logic_hybrid_ctf_cp" ) );
+ if ( pHybridMap_CTF_CP )
+ {
+ m_bPlayingHybrid_CTF_CP.Set( true );
+ }
+
+ CTFHolidayEntity *pHolidayEntity = dynamic_cast<CTFHolidayEntity*> ( gEntList.FindEntityByClassname( NULL, "tf_logic_holiday" ) );
+ if ( pHolidayEntity )
+ {
+ m_nMapHolidayType.Set( pHolidayEntity->GetHolidayType() );
+ }
+
+ // bot roster
+ m_hBlueBotRoster = NULL;
+ m_hRedBotRoster = NULL;
+ CHandle<CTFBotRoster> hBotRoster = dynamic_cast< CTFBotRoster* >( gEntList.FindEntityByClassname( NULL, "bot_roster" ) );
+ while ( hBotRoster != NULL )
+ {
+ if ( FStrEq( hBotRoster->m_teamName.ToCStr(), "blue" ) )
+ {
+ m_hBlueBotRoster = hBotRoster;
+ }
+ else if ( FStrEq( hBotRoster->m_teamName.ToCStr(), "red" ) )
+ {
+ m_hRedBotRoster = hBotRoster;
+ }
+ else
+ {
+ if ( m_hBlueBotRoster == NULL )
+ {
+ m_hBlueBotRoster = hBotRoster;
+ }
+ if ( m_hRedBotRoster == NULL )
+ {
+ m_hRedBotRoster = hBotRoster;
+ }
+ }
+ hBotRoster = dynamic_cast< CTFBotRoster* >( gEntList.FindEntityByClassname( hBotRoster, "bot_roster" ) );
+ }
+
+ CHandle<CCPTimerLogic> hCPTimer = dynamic_cast< CCPTimerLogic* >( gEntList.FindEntityByClassname( NULL, "tf_logic_cp_timer" ) );
+ while ( hCPTimer != NULL )
+ {
+ m_CPTimerEnts.AddToTail( hCPTimer );
+ hCPTimer = dynamic_cast< CCPTimerLogic* >( gEntList.FindEntityByClassname( hCPTimer, "tf_logic_cp_timer" ) );
+ }
+
+ // hide from the master server if this game is a training game
+ // or offline practice
+ if ( IsInTraining() || TheTFBots().IsInOfflinePractice() || IsInItemTestingMode() )
+ {
+ hide_server.SetValue( true );
+ }
+
+ m_bVoteCalled = false;
+ m_bServerVoteOnReset = false;
+ m_flVoteCheckThrottle = 0;
+
+#ifdef STAGING_ONLY
+ // Dynamically create an upgrade entity outside MvM
+ if ( tf_bountymode.GetBool() && !IsBountyMode() )
+ {
+ SetBountyMode( true );
+ }
+#endif // STAGING_ONLY
+
+ if ( tf_powerup_mode.GetBool() )
+ {
+ if ( !IsPowerupMode() )
+ {
+ SetPowerupMode( true );
+ }
+ }
+
+// if ( !IsInTournamentMode() )
+// {
+// CExtraMapEntity::SpawnExtraModel();
+// }
+
+ // If leaving MvM for any other game mode, clean up any sticky UI/state
+ if ( IsInTournamentMode() && m_nGameType != TF_GAMETYPE_MVM && g_TFGameModeHistory.GetPrevState() == TF_GAMETYPE_MVM )
+ {
+ mp_tournament.SetValue( false );
+ }
+
+ if ( GameModeUsesUpgrades() && g_pPopulationManager == NULL )
+ {
+ (CPopulationManager *)CreateEntityByName( "info_populator" );
+ }
+
+ if ( tf_gamemode_tc.GetBool() || tf_gamemode_sd.GetBool() || tf_gamemode_pd.GetBool() || m_bPlayingMedieval )
+ {
+ tf_gamemode_misc.SetValue( 1 );
+ }
+
+ CBaseEntity *pStageLogic = gEntList.FindEntityByName( NULL, "competitive_stage_logic_case" );
+ if ( pStageLogic )
+ {
+ m_bMapHasMatchSummaryStage.Set( true );
+ }
+
+ m_bCompetitiveMode.Set( false );
+
+ const IMatchGroupDescription *pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc )
+ {
+ pMatchDesc->InitGameRulesSettings();
+ }
+
+ CLogicMannPower *pLogicMannPower = dynamic_cast< CLogicMannPower* > ( gEntList.FindEntityByClassname( NULL, "tf_logic_mannpower" ) );
+ tf_powerup_mode.SetValue( pLogicMannPower ? 1 : 0 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::AllowDamage( CBaseEntity *pVictim, const CTakeDamageInfo &info )
+{
+ bool bRetVal = true;
+
+ if ( ( State_Get() == GR_STATE_TEAM_WIN ) && pVictim )
+ {
+ if ( pVictim->GetTeamNumber() == GetWinningTeam() )
+ {
+ CBaseTrigger *pTrigger = dynamic_cast< CBaseTrigger *>( info.GetInflictor() );
+
+ // we don't want players on the winning team to be
+ // hurt by team-specific trigger_hurt entities during the bonus time
+ if ( pTrigger && pTrigger->UsesFilter() )
+ {
+ bRetVal = false;
+ }
+ }
+ }
+
+ return bRetVal;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetTeamGoalString( int iTeam, const char *pszGoal )
+{
+ if ( iTeam == TF_TEAM_RED )
+ {
+ if ( !pszGoal || !pszGoal[0] )
+ {
+ m_pszTeamGoalStringRed.GetForModify()[0] = '\0';
+ }
+ else
+ {
+ if ( Q_stricmp( m_pszTeamGoalStringRed.Get(), pszGoal ) )
+ {
+ Q_strncpy( m_pszTeamGoalStringRed.GetForModify(), pszGoal, MAX_TEAMGOAL_STRING );
+ }
+ }
+ }
+ else if ( iTeam == TF_TEAM_BLUE )
+ {
+ if ( !pszGoal || !pszGoal[0] )
+ {
+ m_pszTeamGoalStringBlue.GetForModify()[0] = '\0';
+ }
+ else
+ {
+ if ( Q_stricmp( m_pszTeamGoalStringBlue.Get(), pszGoal ) )
+ {
+ Q_strncpy( m_pszTeamGoalStringBlue.GetForModify(), pszGoal, MAX_TEAMGOAL_STRING );
+ }
+ }
+ }
+}
+
+//=============================================================================
+// HPE_BEGIN:
+// [msmith] Added a HUD type so that we can have the hud independent from the
+// game type. This is useful in training where we want a training hud
+// Instead of the other types of HUD.
+//=============================================================================
+//-----------------------------------------------------------------------------
+// Purpose: Set a HUD type.
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetHUDType( int nHudType )
+{
+ if ( nHudType != TF_HUDTYPE_ARENA )
+ {
+ if ( nHudType >= TF_HUDTYPE_UNDEFINED && nHudType <= TF_HUDTYPE_TRAINING )
+ {
+ m_nHudType.Set( nHudType );
+ }
+ }
+}
+//=============================================================================
+// HPE_END
+//=============================================================================
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::RoundCleanupShouldIgnore( CBaseEntity *pEnt )
+{
+ if ( FindInList( s_PreserveEnts, pEnt->GetClassname() ) )
+ return true;
+
+ //There has got to be a better way of doing this.
+ if ( Q_strstr( pEnt->GetClassname(), "tf_weapon_" ) )
+ return true;
+
+ return BaseClass::RoundCleanupShouldIgnore( pEnt );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldCreateEntity( const char *pszClassName )
+{
+ if ( FindInList( s_PreserveEnts, pszClassName ) )
+ return false;
+
+ return BaseClass::ShouldCreateEntity( pszClassName );
+}
+
+const char* CTFGameRules::GetStalemateSong( int nTeam )
+{
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ return (nTeam == TF_TEAM_RED)
+ ? "Announcer.Helltower_Hell_Red_Stalemate"
+ : "Announcer.Helltower_Hell_Blue_Stalemate";
+ }
+ else if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_DOOMSDAY ) )
+ {
+ return "Announcer.SD_Event_MurderedToStalemate";
+ }
+
+ return "Game.Stalemate";
+}
+
+const char* CTFGameRules::WinSongName( int nTeam )
+{
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ return (nTeam == TF_TEAM_RED)
+ ? "Announcer.Helltower_Hell_Red_Win"
+ : "Announcer.Helltower_Hell_Blue_Win";
+ }
+
+ return "Game.YourTeamWon";
+}
+
+
+const char* CTFGameRules::LoseSongName( int nTeam )
+{
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ return (nTeam == TF_TEAM_RED)
+ ? "Announcer.Helltower_Hell_Red_Lose"
+ : "Announcer.Helltower_Hell_Blue_Lose";
+ }
+ else if ( IsMannVsMachineMode() )
+ {
+ return "music.mvm_lost_wave";
+ }
+ else
+ {
+ return BaseClass::LoseSongName( nTeam );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::CleanUpMap( void )
+{
+#ifdef GAME_DLL
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( !pTFPlayer )
+ continue;
+
+ // Remove all player conditions to prevent some dependency bugs
+ pTFPlayer->m_Shared.RemoveAllCond();
+ }
+#endif // GAME_DLL
+
+ BaseClass::CleanUpMap();
+
+ if ( HLTVDirector() )
+ {
+ HLTVDirector()->BuildCameraList();
+ }
+
+#ifdef GAME_DLL
+ m_hasSpawnedToy = false;
+ for ( int i = 0; i < TF_TEAM_COUNT; i++ )
+ {
+ m_bHasSpawnedSoccerBall[i] = false;
+ }
+
+ // If we're in a mode with upgrades, we force the players to recreate their weapons on next spawn.
+ // This clears out any weapon upgrades they had in the previous round.
+ if ( g_hUpgradeEntity )
+ {
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( !pTFPlayer )
+ continue;
+
+ pTFPlayer->ForceItemRemovalOnRespawn();
+
+ // Remove all player upgrades as well
+ pTFPlayer->RemovePlayerAttributes( false );
+ }
+ }
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::RecalculateControlPointState( void )
+{
+ Assert( ObjectiveResource() );
+
+ if ( !g_hControlPointMasters.Count() )
+ return;
+
+ if ( g_pObjectiveResource && g_pObjectiveResource->PlayingMiniRounds() )
+ return;
+
+ for ( int iTeam = LAST_SHARED_TEAM+1; iTeam < GetNumberOfTeams(); iTeam++ )
+ {
+ int iFarthestPoint = GetFarthestOwnedControlPoint( iTeam, true );
+ if ( iFarthestPoint == -1 )
+ continue;
+
+ // Now enable all spawn points for that spawn point
+ for ( int i=0; i<ITFTeamSpawnAutoList::AutoList().Count(); ++i )
+ {
+ CTFTeamSpawn *pTFSpawn = static_cast< CTFTeamSpawn* >( ITFTeamSpawnAutoList::AutoList()[i] );
+ if ( pTFSpawn->GetControlPoint() )
+ {
+ if ( pTFSpawn->GetTeamNumber() == iTeam )
+ {
+ if ( pTFSpawn->GetControlPoint()->GetPointIndex() == iFarthestPoint )
+ {
+ pTFSpawn->SetDisabled( false );
+ }
+ else
+ {
+ pTFSpawn->SetDisabled( true );
+ }
+ }
+ }
+ }
+ }
+}
+
+DECLARE_AUTO_LIST( ITFTeleportLocationAutoList )
+class CTFTeleportLocation : public CPointEntity, public ITFTeleportLocationAutoList
+{
+public:
+ DECLARE_CLASS( CTFTeleportLocation, CPointEntity );
+};
+
+IMPLEMENT_AUTO_LIST( ITFTeleportLocationAutoList );
+
+LINK_ENTITY_TO_CLASS( tf_teleport_location, CTFTeleportLocation );
+
+void SpawnRunes( void )
+{
+ // Spawn power-up runes when the round starts. Choice of spawn location and Powerup Type is random
+ CUtlVector< CTFInfoPowerupSpawn* > vecSpawnPoints;
+ for ( int i = 0; i < IInfoPowerupSpawnAutoList::AutoList().Count(); i++ )
+ {
+ CTFInfoPowerupSpawn *pSpawnPoint = static_cast< CTFInfoPowerupSpawn* >( IInfoPowerupSpawnAutoList::AutoList()[i] );
+ if ( !pSpawnPoint->IsDisabled() && !pSpawnPoint->HasRune() )
+ {
+ vecSpawnPoints.AddToTail( pSpawnPoint );
+ }
+ }
+
+ Assert( vecSpawnPoints.Count() > 0 ); // We need at least one valid info_powerup_spawn
+
+ // Warn if there aren't enough valid info_powerup_spawns to spawn all powerups
+ if ( vecSpawnPoints.Count() < RUNE_TYPES_MAX )
+ {
+ Warning( "Warning: Not enough valid info_powerup_spawn locations found. You need a minimum of %i valid locations to spawn all Powerups.\n", RUNE_TYPES_MAX );
+ }
+
+ // try to spawn each rune type
+ for ( int nRuneTypes = 0; nRuneTypes < RUNE_TYPES_MAX && vecSpawnPoints.Count() > 0; nRuneTypes++ )
+ {
+ int index = RandomInt( 0, vecSpawnPoints.Count() - 1 );
+ CTFInfoPowerupSpawn *pSpawnPoint = vecSpawnPoints[index];
+ CTFRune *pNewRune = CTFRune::CreateRune( pSpawnPoint->GetAbsOrigin(), (RuneTypes_t) nRuneTypes, TEAM_ANY, false, false );
+ pSpawnPoint->SetRune( pNewRune );
+ vecSpawnPoints.Remove( index );
+ }
+}
+
+#ifdef STAGING_ONLY
+// Force spawn runes for testing
+CON_COMMAND_F( tf_force_spawn_runes, "For testing.", FCVAR_CHEAT )
+{
+ SpawnRunes();
+}
+#endif
+
+void CTFGameRules::RespawnPlayers( bool bForceRespawn, bool bTeam, int iTeam )
+{
+ // Skip the respawn at the beginning of a round in casual/comp mode since we already
+ // handled it when the pre-round doors closed over the players' views
+ if ( IsCompetitiveMode() && ( GetRoundsPlayed() == 0 ) && bForceRespawn && ( State_Get() == GR_STATE_BETWEEN_RNDS || State_Get() == GR_STATE_PREROUND ) )
+ {
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( !pMaster || !pMaster->PlayingMiniRounds() || ( pMaster->GetCurrentRoundIndex() == 0 ) )
+ return;
+ }
+
+ BaseClass::RespawnPlayers( bForceRespawn, bTeam, iTeam );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Called when a new round is being initialized
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetupOnRoundStart( void )
+{
+ for ( int i = 0; i < MAX_TEAMS; i++ )
+ {
+ ObjectiveResource()->SetBaseCP( -1, i );
+ }
+
+ for ( int i = 0; i < TF_TEAM_COUNT; i++ )
+ {
+ m_iNumCaps[i] = 0;
+ }
+
+ SetOvertime( false );
+
+ m_hRedKothTimer.Set( NULL );
+ m_hBlueKothTimer.Set( NULL );
+
+ // Let all entities know that a new round is starting
+ CBaseEntity *pEnt = gEntList.FirstEnt();
+ while( pEnt )
+ {
+ variant_t emptyVariant;
+ pEnt->AcceptInput( "RoundSpawn", NULL, NULL, emptyVariant, 0 );
+
+ pEnt = gEntList.NextEnt( pEnt );
+ }
+
+ // All entities have been spawned, now activate them
+ m_areHealthAndAmmoVectorsReady = false;
+ m_ammoVector.RemoveAll();
+ m_healthVector.RemoveAll();
+
+ pEnt = gEntList.FirstEnt();
+ while( pEnt )
+ {
+ variant_t emptyVariant;
+ pEnt->AcceptInput( "RoundActivate", NULL, NULL, emptyVariant, 0 );
+
+ pEnt = gEntList.NextEnt( pEnt );
+ }
+
+ if ( g_pObjectiveResource && !g_pObjectiveResource->PlayingMiniRounds() )
+ {
+ // Find all the control points with associated spawnpoints
+ memset( m_bControlSpawnsPerTeam, 0, sizeof(bool) * MAX_TEAMS * MAX_CONTROL_POINTS );
+ for ( int i=0; i<ITFTeamSpawnAutoList::AutoList().Count(); ++i )
+ {
+ CTFTeamSpawn *pTFSpawn = static_cast< CTFTeamSpawn* >( ITFTeamSpawnAutoList::AutoList()[i] );
+ if ( pTFSpawn->GetControlPoint() )
+ {
+ m_bControlSpawnsPerTeam[ pTFSpawn->GetTeamNumber() ][ pTFSpawn->GetControlPoint()->GetPointIndex() ] = true;
+ pTFSpawn->SetDisabled( true );
+ }
+ }
+
+ RecalculateControlPointState();
+
+ SetRoundOverlayDetails();
+ }
+
+ //Do any round specific setup for training logic (resetting the score, messages, etc).
+ if ( IsInTraining() )
+ {
+ if ( m_hTrainingModeLogic )
+ {
+ m_hTrainingModeLogic->SetupOnRoundStart();
+ }
+ }
+
+ m_szMostRecentCappers[0] = 0;
+ m_halloweenBossTimer.Invalidate();
+ m_ghostVector.RemoveAll();
+
+ m_zombieMobTimer.Invalidate();
+ m_zombiesLeftToSpawn = 0;
+
+ SetIT( NULL );
+ SetBirthdayPlayer( NULL );
+
+ if ( g_pMonsterResource )
+ {
+ g_pMonsterResource->HideBossHealthMeter();
+ }
+
+#ifdef TF_RAID_MODE
+ if ( IsBossBattleMode() )
+ {
+ CTFTeam *enemyTeam = GetGlobalTFTeam( TF_TEAM_RED );
+ for( int i=0; i<enemyTeam->GetNumPlayers(); ++i )
+ {
+ CTFPlayer *who = ToTFPlayer( enemyTeam->GetPlayer( i ) );
+
+ who->ChangeTeam( TEAM_SPECTATOR, false, true );
+ who->RemoveAllObjects();
+ }
+ }
+#endif // TF_RAID_MODE
+
+ if ( IsMannVsMachineMode() )
+ {
+ if ( g_hMannVsMachineLogic )
+ {
+ g_hMannVsMachineLogic->SetupOnRoundStart();
+ }
+ }
+
+ m_redPayloadToPush = NULL;
+ m_bluePayloadToPush = NULL;
+ m_redPayloadToBlock = NULL;
+ m_bluePayloadToBlock = NULL;
+
+ // Tell the clients to recalculate the holiday
+ IGameEvent *event = gameeventmanager->CreateEvent( "recalculate_holidays" );
+ if ( event )
+ {
+ gameeventmanager->FireEvent( event );
+ }
+
+ UTIL_CalculateHolidays();
+
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ m_helltowerTimer.Start( HELLTOWER_TIMER_INTERVAL );
+ }
+
+ // reset hell state
+ SetPlayersInHell( false );
+
+ if ( IsPowerupMode() )
+ {
+ // Reset imbalance detection scores
+ m_nPowerupKillsBlueTeam = 0;
+ m_nPowerupKillsRedTeam = 0;
+ SpawnRunes();
+ }
+
+ m_hHolidayLogic = dynamic_cast<CTFHolidayEntity*> ( gEntList.FindEntityByClassname( NULL, "tf_logic_holiday" ) );
+ if ( m_hHolidayLogic.IsValid() )
+ {
+ m_hHolidayLogic->ResetWinner();
+ }
+
+ // cache off teleport locations and remove entities to save edicts
+ m_mapTeleportLocations.PurgeAndDeleteElements();
+ for ( int i=0; i<ITFTeleportLocationAutoList::AutoList().Count(); ++i )
+ {
+ CTFTeleportLocation *pLocation = static_cast< CTFTeleportLocation* >( ITFTeleportLocationAutoList::AutoList()[i] );
+
+ int iMap = m_mapTeleportLocations.Find( pLocation->GetEntityName() );
+ if ( !m_mapTeleportLocations.IsValidIndex( iMap ) )
+ {
+ CUtlVector< TeleportLocation_t > *pNew = new CUtlVector< TeleportLocation_t >;
+ iMap = m_mapTeleportLocations.Insert( pLocation->GetEntityName(), pNew );
+ }
+
+ CUtlVector< TeleportLocation_t > *pLocations = m_mapTeleportLocations[iMap];
+ int iLocation = pLocations->AddToTail();
+ pLocations->Element( iLocation ).m_vecPosition = pLocation->GetAbsOrigin();
+ pLocations->Element( iLocation ).m_qAngles = pLocation->GetAbsAngles();
+
+ UTIL_Remove( pLocation );
+ }
+
+ // swap our train model for the EOTL holiday
+ if ( IsHolidayActive( kHoliday_EOTL ) )
+ {
+ for ( int i = 0; i < IPhysicsPropAutoList::AutoList().Count(); i++ )
+ {
+ CPhysicsProp *pPhysicsProp = static_cast<CPhysicsProp*>( IPhysicsPropAutoList::AutoList()[i] );
+ const char *pszTemp = pPhysicsProp->GetModelName().ToCStr();
+
+ if ( FStrEq( pszTemp, "models/props_trainyard/bomb_cart.mdl" ) )
+ {
+ pPhysicsProp->SetModel( "models/props_trainyard/bomb_eotl_blue.mdl" );
+ }
+ else if ( FStrEq( pszTemp, "models/props_trainyard/bomb_cart_red.mdl" ) )
+ {
+ pPhysicsProp->SetModel( "models/props_trainyard/bomb_eotl_red.mdl" );
+ }
+ }
+ }
+
+ m_flMatchSummaryTeleportTime = -1.f;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::RestartTournament( void )
+{
+ BaseClass::RestartTournament();
+
+ if ( GetStopWatchTimer() )
+ {
+ UTIL_Remove( GetStopWatchTimer() );
+ }
+
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( !pTFPlayer || !pTFPlayer->IsAlive() )
+ continue;
+
+ pTFPlayer->m_Shared.RemoveCond( TF_COND_CRITBOOSTED, true );
+ pTFPlayer->m_Shared.RemoveCond( TF_COND_CRITBOOSTED_BONUS_TIME, true );
+ pTFPlayer->m_Shared.RemoveCond( TF_COND_CRITBOOSTED_CTF_CAPTURE, true );
+ }
+
+ ItemSystem()->ReloadWhitelist();
+
+ ResetPlayerAndTeamReadyState();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::HandleTeamScoreModify( int iTeam, int iScore )
+{
+ BaseClass::HandleTeamScoreModify( iTeam, iScore );
+
+ if ( IsInStopWatch() == true )
+ {
+ if ( GetStopWatchTimer() )
+ {
+ if ( GetStopWatchTimer()->IsWatchingTimeStamps() == true )
+ {
+ GetStopWatchTimer()->SetStopWatchTimeStamp();
+ }
+
+ StopWatchModeThink();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::StopWatchShouldBeTimedWin_Calculate( void )
+{
+ m_bStopWatchShouldBeTimedWin = false;
+
+ if ( IsInTournamentMode() && IsInStopWatch() && ObjectiveResource() )
+ {
+ int iStopWatchTimer = ObjectiveResource()->GetStopWatchTimer();
+ CTeamRoundTimer *pStopWatch = dynamic_cast< CTeamRoundTimer* >( UTIL_EntityByIndex( iStopWatchTimer ) );
+ if ( pStopWatch && !pStopWatch->IsWatchingTimeStamps() )
+ {
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+
+ if ( pMaster == NULL )
+ return;
+
+ int iNumPoints = pMaster->GetNumPoints();
+
+ CTFTeam *pAttacker = NULL;
+ CTFTeam *pDefender = NULL;
+
+ for ( int i = LAST_SHARED_TEAM+1; i < GetNumberOfTeams(); i++ )
+ {
+ CTFTeam *pTeam = GetGlobalTFTeam( i );
+
+ if ( pTeam )
+ {
+ if ( pTeam->GetRole() == TEAM_ROLE_DEFENDERS )
+ {
+ pDefender = pTeam;
+ }
+
+ if ( pTeam->GetRole() == TEAM_ROLE_ATTACKERS )
+ {
+ pAttacker = pTeam;
+ }
+ }
+ }
+
+ if ( pAttacker == NULL || pDefender == NULL )
+ return;
+
+ m_bStopWatchShouldBeTimedWin = ( pDefender->GetScore() == iNumPoints );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::StopWatchShouldBeTimedWin( void )
+{
+ StopWatchShouldBeTimedWin_Calculate();
+ return m_bStopWatchShouldBeTimedWin;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::StopWatchModeThink( void )
+{
+ if ( IsInTournamentMode() == false || IsInStopWatch() == false )
+ return;
+
+ if ( GetStopWatchTimer() == NULL )
+ return;
+
+ CTeamRoundTimer *pTimer = GetStopWatchTimer();
+
+ bool bWatchingCaps = pTimer->IsWatchingTimeStamps();
+
+ CTFTeam *pAttacker = NULL;
+ CTFTeam *pDefender = NULL;
+
+ for ( int i = LAST_SHARED_TEAM+1; i < GetNumberOfTeams(); i++ )
+ {
+ CTFTeam *pTeam = GetGlobalTFTeam( i );
+
+ if ( pTeam )
+ {
+ if ( pTeam->GetRole() == TEAM_ROLE_DEFENDERS )
+ {
+ pDefender = pTeam;
+ }
+
+ if ( pTeam->GetRole() == TEAM_ROLE_ATTACKERS )
+ {
+ pAttacker = pTeam;
+ }
+ }
+ }
+
+ if ( pAttacker == NULL || pDefender == NULL )
+ return;
+
+ m_bStopWatchWinner.Set( false );
+
+ if ( bWatchingCaps == false )
+ {
+ if ( pTimer->GetTimeRemaining() <= 0.0f )
+ {
+ if ( StopWatchShouldBeTimedWin() )
+ {
+ if ( pAttacker->GetScore() < pDefender->GetScore() )
+ {
+ m_bStopWatchWinner.Set( true );
+ SetWinningTeam( pDefender->GetTeamNumber(), WINREASON_DEFEND_UNTIL_TIME_LIMIT, true, true );
+ }
+ }
+ else
+ {
+ if ( pAttacker->GetScore() > pDefender->GetScore() )
+ {
+ m_bStopWatchWinner.Set( true );
+ SetWinningTeam( pAttacker->GetTeamNumber(), WINREASON_ALL_POINTS_CAPTURED, true, true );
+ }
+ }
+
+ if ( pTimer->IsTimerPaused() == false )
+ {
+ variant_t sVariant;
+ pTimer->AcceptInput( "Pause", NULL, NULL, sVariant, 0 );
+ }
+
+ m_nStopWatchState.Set( STOPWATCH_OVERTIME );
+ }
+ else
+ {
+ if ( pAttacker->GetScore() >= pDefender->GetScore() )
+ {
+ m_bStopWatchWinner.Set( true );
+ SetWinningTeam( pAttacker->GetTeamNumber(), WINREASON_ALL_POINTS_CAPTURED, true, true );
+ }
+ }
+ }
+ else
+ {
+ if ( pTimer->GetTimeRemaining() <= 0.0f )
+ {
+ m_nStopWatchState.Set( STOPWATCH_CAPTURE_TIME_NOT_SET );
+ }
+ else
+ {
+ m_nStopWatchState.Set( STOPWATCH_RUNNING );
+ }
+ }
+
+ if ( m_bStopWatchWinner == true )
+ {
+ UTIL_Remove( pTimer );
+ m_hStopWatchTimer = NULL;
+ m_flStopWatchTotalTime = -1.0f;
+ m_bStopWatch = false;
+ m_nStopWatchState.Set( STOPWATCH_CAPTURE_TIME_NOT_SET );
+
+ ShouldResetRoundsPlayed( false );
+ ShouldResetScores( true, false );
+ ResetScores();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::ManageStopwatchTimer( bool bInSetup )
+{
+ if ( IsInTournamentMode() == false )
+ return;
+
+ if ( mp_tournament_stopwatch.GetBool() == false )
+ return;
+
+ bool bAttacking = false;
+ bool bDefending = false;
+
+ for ( int i = LAST_SHARED_TEAM+1; i < GetNumberOfTeams(); i++ )
+ {
+ CTFTeam *pTeam = GetGlobalTFTeam( i );
+
+ if ( pTeam )
+ {
+ if ( pTeam->GetRole() == TEAM_ROLE_DEFENDERS )
+ {
+ bDefending = true;
+ }
+
+ if ( pTeam->GetRole() == TEAM_ROLE_ATTACKERS )
+ {
+ bAttacking = true;
+ }
+ }
+ }
+
+ if ( bDefending == true && bAttacking == true )
+ {
+ SetInStopWatch( true );
+ }
+ else
+ {
+ SetInStopWatch( false );
+ }
+
+ if ( IsInStopWatch() == true )
+ {
+ if ( m_hStopWatchTimer == NULL )
+ {
+ variant_t sVariant;
+ CTeamRoundTimer *pStopWatch = (CTeamRoundTimer*)CBaseEntity::CreateNoSpawn( "team_round_timer", vec3_origin, vec3_angle );
+ m_hStopWatchTimer = pStopWatch;
+
+ pStopWatch->SetName( MAKE_STRING("zz_stopwatch_timer") );
+ pStopWatch->SetShowInHud( false );
+ pStopWatch->SetStopWatch( true );
+
+ if ( m_flStopWatchTotalTime < 0.0f )
+ {
+ pStopWatch->SetCaptureWatchState( true );
+ DispatchSpawn( pStopWatch );
+
+ pStopWatch->AcceptInput( "Enable", NULL, NULL, sVariant, 0 );
+ }
+ else
+ {
+ DispatchSpawn( pStopWatch );
+ pStopWatch->SetCaptureWatchState( false );
+
+
+ sVariant.SetInt( m_flStopWatchTotalTime );
+ pStopWatch->AcceptInput( "Enable", NULL, NULL, sVariant, 0 );
+ pStopWatch->AcceptInput( "SetTime", NULL, NULL, sVariant, 0 );
+ pStopWatch->SetAutoCountdown( true );
+ }
+
+ if ( bInSetup == true )
+ {
+ pStopWatch->AcceptInput( "Pause", NULL, NULL, sVariant, 0 );
+ }
+
+ ObjectiveResource()->SetStopWatchTimer( pStopWatch );
+ }
+ else
+ {
+ if ( bInSetup == false )
+ {
+ variant_t sVariant;
+ m_hStopWatchTimer->AcceptInput( "Resume", NULL, NULL, sVariant, 0 );
+ }
+ else
+ {
+ variant_t sVariant;
+ m_hStopWatchTimer->AcceptInput( "Pause", NULL, NULL, sVariant, 0 );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetSetup( bool bSetup )
+{
+ if ( m_bInSetup == bSetup )
+ return;
+
+ BaseClass::SetSetup( bSetup );
+
+ ManageStopwatchTimer( bSetup );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Called when a new round is off and running
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetupOnRoundRunning( void )
+{
+ // Let out control point masters know that the round has started
+ for ( int i = 0; i < g_hControlPointMasters.Count(); i++ )
+ {
+ variant_t emptyVariant;
+ if ( g_hControlPointMasters[i] )
+ {
+ g_hControlPointMasters[i]->AcceptInput( "RoundStart", NULL, NULL, emptyVariant, 0 );
+ }
+ }
+
+ // Reset player speeds after preround lock
+ CTFPlayer *pPlayer;
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( !pPlayer )
+ continue;
+
+ pPlayer->TeamFortress_SetSpeed();
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ if ( !IsInWaitingForPlayers() )
+ {
+ pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_ROUND_START );
+ }
+
+ }
+ else if ( !IsHalloweenScenario( HALLOWEEN_SCENARIO_DOOMSDAY ) )
+ {
+ if ( IsCompetitiveMode() )
+ {
+ pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_ROUND_START_COMP );
+ }
+ else
+ {
+ pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_ROUND_START );
+ }
+ }
+
+ // warp the coach to student
+ if ( pPlayer->GetStudent() )
+ {
+ pPlayer->SetObserverTarget( pPlayer->GetStudent() );
+ pPlayer->StartObserverMode( OBS_MODE_CHASE );
+ }
+
+ if ( FNullEnt( pPlayer->edict() ) )
+ continue;
+
+ pPlayer->m_Shared.ResetRoundScores();
+
+ // Store the class they started the round as (tf_player captures everything after this)
+ if ( pPlayer->GetTeamNumber() >= FIRST_GAME_TEAM )
+ {
+ CSteamID steamID;
+ pPlayer->GetSteamID( &steamID );
+
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ if ( pMatch )
+ {
+ CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( steamID );
+ if ( pMatchPlayer )
+ {
+ pMatchPlayer->UpdateClassesPlayed( pPlayer->GetPlayerClass()->GetClassIndex() );
+ pMatchPlayer->bPlayed = true;
+ }
+ }
+ }
+ }
+
+ if ( IsPlayingSpecialDeliveryMode() && !IsInWaitingForPlayers() )
+ {
+ if ( !IsHalloweenScenario( HALLOWEEN_SCENARIO_DOOMSDAY ) )
+ {
+ BroadcastSound( 255, "Announcer.SD_RoundStart" );
+ }
+ }
+
+ if ( IsMannVsMachineMode() && g_pPopulationManager )
+ {
+ IGameEvent *event = gameeventmanager->CreateEvent( "mvm_begin_wave" );
+ if ( event )
+ {
+ event->SetInt( "wave_index", g_pPopulationManager->GetWaveNumber() );
+ event->SetInt( "max_waves", g_pPopulationManager->GetTotalWaveCount() );
+ event->SetInt( "advanced", g_pPopulationManager->IsAdvancedPopFile() ? 1 : 0 );
+ gameeventmanager->FireEvent( event );
+ }
+ }
+
+ if ( IsCompetitiveMode() && !( GetActiveRoundTimer() && ( GetActiveRoundTimer()->GetSetupTimeLength() > 0 ) ) )
+ {
+ // Announcer VO
+ if ( ( TFTeamMgr()->GetTeam( TF_TEAM_BLUE )->GetScore() == ( mp_winlimit.GetInt() - 1 ) ) &&
+ ( TFTeamMgr()->GetTeam( TF_TEAM_RED )->GetScore() == ( mp_winlimit.GetInt() - 1 ) ) )
+ {
+ BroadcastSound( 255, "Announcer.CompFinalGameBeginsFight" );
+ }
+ else
+ {
+ BroadcastSound( 255, "Announcer.CompGameBeginsFight" );
+ }
+ }
+
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ if ( pMatch && IsMatchTypeCompetitive() )
+ {
+ static ConVarRef tf_bot_quota( "tf_bot_quota" );
+ tf_bot_quota.SetValue( (int)pMatch->GetCanonicalMatchSize() );
+ static ConVarRef tf_bot_quota_mode( "tf_bot_quota_mode" );
+ tf_bot_quota_mode.SetValue( "fill" );
+ }
+
+ if ( m_hGamerulesProxy )
+ {
+ m_hGamerulesProxy->StateEnterRoundRunning();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Called before a new round is started (so the previous round can end)
+//-----------------------------------------------------------------------------
+void CTFGameRules::PreviousRoundEnd( void )
+{
+ // before we enter a new round, fire the "end output" for the previous round
+ if ( g_hControlPointMasters.Count() && g_hControlPointMasters[0] )
+ {
+ g_hControlPointMasters[0]->FireRoundEndOutput();
+ }
+
+ m_iPreviousRoundWinners = GetWinningTeam();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Called when a round has entered stalemate mode (timer has run out)
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetupOnStalemateStart( void )
+{
+ // Remove everyone's objects
+ RemoveAllProjectilesAndBuildings();
+
+ if ( IsInArenaMode() == false )
+ {
+ // Respawn all the players
+ RespawnPlayers( true );
+
+ // Disable all the active health packs in the world
+ m_hDisabledHealthKits.Purge();
+ CHealthKit *pHealthPack = gEntList.NextEntByClass( (CHealthKit *)NULL );
+ while ( pHealthPack )
+ {
+ if ( !pHealthPack->IsDisabled() )
+ {
+ pHealthPack->SetDisabled( true );
+ m_hDisabledHealthKits.AddToTail( pHealthPack );
+ }
+ pHealthPack = gEntList.NextEntByClass( pHealthPack );
+ }
+ }
+ else
+ {
+ CArenaLogic *pArenaLogic = dynamic_cast< CArenaLogic * > (gEntList.FindEntityByClassname( NULL, "tf_logic_arena" ) );
+
+ if ( pArenaLogic )
+ {
+ pArenaLogic->m_OnArenaRoundStart.FireOutput( pArenaLogic, pArenaLogic );
+
+ if ( tf_arena_override_cap_enable_time.GetFloat() > 0 )
+ {
+ m_flCapturePointEnableTime = gpGlobals->curtime + tf_arena_override_cap_enable_time.GetFloat();
+ }
+ else
+ {
+ m_flCapturePointEnableTime = gpGlobals->curtime + pArenaLogic->m_flTimeToEnableCapPoint;
+ }
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "arena_round_start" );
+ if ( event )
+ {
+ gameeventmanager->FireEvent( event );
+ }
+
+ BroadcastSound( 255, "Announcer.AM_RoundStartRandom" );
+ }
+ }
+
+ CTFPlayer *pPlayer;
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( !pPlayer )
+ continue;
+
+ if ( IsInArenaMode() == true )
+ {
+ pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_ROUND_START );
+ pPlayer->TeamFortress_SetSpeed();
+ }
+ else
+ {
+ pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_SUDDENDEATH_START );
+ }
+ }
+
+ m_flStalemateStartTime = gpGlobals->curtime;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetupOnStalemateEnd( void )
+{
+ // Reenable all the health packs we disabled
+ for ( int i = 0; i < m_hDisabledHealthKits.Count(); i++ )
+ {
+ if ( m_hDisabledHealthKits[i] )
+ {
+ m_hDisabledHealthKits[i]->SetDisabled( false );
+ }
+ }
+
+ m_hDisabledHealthKits.Purge();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::InitTeams( void )
+{
+ BaseClass::InitTeams();
+
+ // clear the player class data
+ ResetFilePlayerClassInfoDatabase();
+}
+
+
+class CTraceFilterIgnoreTeammatesWithException : public CTraceFilterSimple
+{
+ DECLARE_CLASS( CTraceFilterIgnoreTeammatesWithException, CTraceFilterSimple );
+public:
+ CTraceFilterIgnoreTeammatesWithException( const IHandleEntity *passentity, int collisionGroup, int iIgnoreTeam, const IHandleEntity *pExceptionEntity )
+ : CTraceFilterSimple( passentity, collisionGroup ), m_iIgnoreTeam( iIgnoreTeam )
+ {
+ m_pExceptionEntity = pExceptionEntity;
+ }
+
+ virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask )
+ {
+ CBaseEntity *pEntity = EntityFromEntityHandle( pServerEntity );
+
+ if ( pServerEntity == m_pExceptionEntity )
+ return true;
+
+ if ( pEntity->IsPlayer() && pEntity->GetTeamNumber() == m_iIgnoreTeam )
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ int m_iIgnoreTeam;
+ const IHandleEntity *m_pExceptionEntity;
+};
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::RadiusDamage( CTFRadiusDamageInfo &info )
+{
+ float flRadSqr = (info.flRadius * info.flRadius);
+
+ int iDamageEnemies = 0;
+ int nDamageDealt = 0;
+ // Some weapons pass a radius of 0, since their only goal is to give blast jumping ability
+ if ( info.flRadius > 0 )
+ {
+ // Find all the entities in the radius, and attempt to damage them.
+ CBaseEntity *pEntity = NULL;
+ for ( CEntitySphereQuery sphere( info.vecSrc, info.flRadius ); (pEntity = sphere.GetCurrentEntity()) != NULL; sphere.NextEntity() )
+ {
+ // Skip the attacker, if we have a RJ radius set. We'll do it post.
+ if ( info.flRJRadius && pEntity == info.dmgInfo->GetAttacker() )
+ 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( info.vecSrc, &vecPos );
+ if ( (info.vecSrc - vecPos).LengthSqr() > flRadSqr )
+ continue;
+
+ int iDamageToEntity = info.ApplyToEntity( pEntity );
+ if ( iDamageToEntity )
+ {
+ // Keep track of any enemies we damaged
+ if ( pEntity->IsPlayer() && !pEntity->InSameTeam( info.dmgInfo->GetAttacker() ) )
+ {
+ nDamageDealt+= iDamageToEntity;
+ iDamageEnemies++;
+ }
+ }
+ }
+ }
+
+ // If we have a set RJ radius, use it to affect the inflictor. This way Rocket Launchers
+ // with modified damage/radius perform like the base rocket launcher when it comes to RJing.
+ if ( info.flRJRadius && info.dmgInfo->GetAttacker() )
+ {
+ // Set our radius & damage to the base RL
+ info.flRadius = info.flRJRadius;
+ CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>(info.dmgInfo->GetWeapon());
+ if ( pWeapon )
+ {
+ float flBaseDamage = pWeapon->GetTFWpnData().GetWeaponData( TF_WEAPON_PRIMARY_MODE ).m_nDamage;
+ info.dmgInfo->SetDamage( flBaseDamage );
+ info.dmgInfo->CopyDamageToBaseDamage();
+ info.dmgInfo->SetDamagedOtherPlayers( iDamageEnemies );
+
+ // If we dealt damage, check radius life leech
+ if ( nDamageDealt > 0 )
+ {
+ // Heal on hits
+ int iModHealthOnHit = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( info.dmgInfo->GetWeapon(), iModHealthOnHit, add_health_on_radius_damage );
+ if ( iModHealthOnHit )
+ {
+ // Scale Health mod with damage dealt, input being the maximum amount of health possible
+ float flScale = Clamp( nDamageDealt / flBaseDamage, 0.f, 1.0f );
+ iModHealthOnHit = (int)( (float)iModHealthOnHit * flScale );
+ int iHealed = info.dmgInfo->GetAttacker()->TakeHealth( iModHealthOnHit, DMG_GENERIC );
+ if ( iHealed )
+ {
+ IGameEvent *event = gameeventmanager->CreateEvent( "player_healonhit" );
+ if ( event )
+ {
+ event->SetInt( "amount", iHealed );
+ event->SetInt( "entindex", info.dmgInfo->GetAttacker()->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 );
+ }
+ }
+ }
+ }
+ }
+
+ // Apply ourselves to our attacker, if we're within range
+ Vector vecPos;
+ info.dmgInfo->GetAttacker()->CollisionProp()->CalcNearestPoint( info.vecSrc, &vecPos );
+ if ( (info.vecSrc - vecPos).LengthSqr() <= (info.flRadius * info.flRadius) )
+ {
+ info.ApplyToEntity( info.dmgInfo->GetAttacker() );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Calculate the damage falloff over distance
+//-----------------------------------------------------------------------------
+void CTFRadiusDamageInfo::CalculateFalloff( void )
+{
+ if ( dmgInfo->GetDamageType() & DMG_RADIUS_MAX )
+ flFalloff = 0.0;
+ else if ( dmgInfo->GetDamageType() & DMG_HALF_FALLOFF )
+ flFalloff = 0.5;
+ else if ( flRadius )
+ flFalloff = dmgInfo->GetDamage() / flRadius;
+ else
+ flFalloff = 1.0;
+
+ CBaseEntity *pWeapon = dmgInfo->GetWeapon();
+ if ( pWeapon != NULL )
+ {
+ float flFalloffMod = 1.f;
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pWeapon, flFalloffMod, mult_dmg_falloff );
+ if ( flFalloffMod != 1.f )
+ {
+ flFalloff += flFalloffMod;
+ }
+ }
+
+ if ( TFGameRules() && TFGameRules()->IsPowerupMode() )
+ {
+ CTFPlayer *pOwner = ToTFPlayer( dmgInfo->GetAttacker() );
+ if ( pOwner && pOwner->m_Shared.GetCarryingRuneType() == RUNE_PRECISION )
+ {
+ flFalloff = 1.0;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Attempt to apply the radius damage to the specified entity
+//-----------------------------------------------------------------------------
+int CTFRadiusDamageInfo::ApplyToEntity( CBaseEntity *pEntity )
+{
+ if ( pEntity == pEntityIgnore || pEntity->m_takedamage == DAMAGE_NO )
+ return 0;
+
+ trace_t tr;
+ CBaseEntity *pInflictor = dmgInfo->GetInflictor();
+
+ // Check that the explosion can 'see' this entity.
+ Vector vecSpot = pEntity->BodyTarget( vecSrc, false );
+ CTraceFilterIgnorePlayers filterPlayers( pInflictor, COLLISION_GROUP_PROJECTILE );
+ CTraceFilterIgnoreFriendlyCombatItems filterCombatItems( pInflictor, COLLISION_GROUP_PROJECTILE, pInflictor->GetTeamNumber() );
+ CTraceFilterChain filter( &filterPlayers, &filterCombatItems );
+
+ UTIL_TraceLine( vecSrc, vecSpot, MASK_RADIUS_DAMAGE, &filter, &tr );
+ if ( tr.startsolid && tr.m_pEnt )
+ {
+ // Return when inside an enemy combat shield and tracing against a player of that team ("absorbed")
+ if ( tr.m_pEnt->IsCombatItem() && pEntity->InSameTeam( tr.m_pEnt ) && ( pEntity != tr.m_pEnt ) )
+ return 0;
+
+ filterPlayers.SetPassEntity( tr.m_pEnt );
+ CTraceFilterChain filterSelf( &filterPlayers, &filterCombatItems );
+ UTIL_TraceLine( vecSrc, vecSpot, MASK_RADIUS_DAMAGE, &filterSelf, &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 )
+ return 0;
+
+ // Adjust the damage - apply falloff.
+ float flAdjustedDamage = 0.0f;
+ float flDistanceToEntity;
+
+ // Rockets store the ent they hit as the enemy and have already dealt full damage to them by this time
+ if ( pInflictor && ( pEntity == pInflictor->GetEnemy() ) )
+ {
+ // Full damage, we hit this entity directly
+ flDistanceToEntity = 0;
+ }
+ else if ( pEntity->IsPlayer() )
+ {
+ // Use whichever is closer, absorigin or worldspacecenter
+ float flToWorldSpaceCenter = ( vecSrc - pEntity->WorldSpaceCenter() ).Length();
+ float flToOrigin = ( vecSrc - pEntity->GetAbsOrigin() ).Length();
+
+ flDistanceToEntity = MIN( flToWorldSpaceCenter, flToOrigin );
+ }
+ else
+ {
+ flDistanceToEntity = ( vecSrc - tr.endpos ).Length();
+ }
+
+ flAdjustedDamage = RemapValClamped( flDistanceToEntity, 0, flRadius, dmgInfo->GetDamage(), dmgInfo->GetDamage() * flFalloff );
+
+ CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>(dmgInfo->GetWeapon());
+
+ // Grenades & Pipebombs do less damage to ourselves.
+ if ( pEntity == dmgInfo->GetAttacker() && pWeapon )
+ {
+ switch( pWeapon->GetWeaponID() )
+ {
+ case TF_WEAPON_PIPEBOMBLAUNCHER :
+ case TF_WEAPON_GRENADELAUNCHER :
+ case TF_WEAPON_CANNON :
+ case TF_WEAPON_STICKBOMB :
+ flAdjustedDamage *= 0.75f;
+ break;
+ }
+ }
+
+ // If we end up doing 0 damage, exit now.
+ if ( flAdjustedDamage <= 0 )
+ return 0;
+
+ // the explosion can 'see' this entity, so hurt them!
+ if (tr.startsolid)
+ {
+ // if we're stuck inside them, fixup the position and distance
+ tr.endpos = vecSrc;
+ tr.fraction = 0.0;
+ }
+
+ CTakeDamageInfo adjustedInfo = *dmgInfo;
+ adjustedInfo.SetDamage( flAdjustedDamage );
+
+ Vector dir = vecSpot - vecSrc;
+ VectorNormalize( dir );
+
+ // If we don't have a damage force, manufacture one
+ if ( adjustedInfo.GetDamagePosition() == vec3_origin || adjustedInfo.GetDamageForce() == vec3_origin )
+ {
+ CalculateExplosiveDamageForce( &adjustedInfo, dir, vecSrc );
+ }
+ else
+ {
+ // Assume the force passed in is the maximum force. Decay it based on falloff.
+ float flForce = adjustedInfo.GetDamageForce().Length() * flFalloff;
+ adjustedInfo.SetDamageForce( dir * flForce );
+ adjustedInfo.SetDamagePosition( vecSrc );
+ }
+
+ adjustedInfo.ScaleDamageForce( m_flForceScale );
+
+ int nDamageTaken = 0;
+ if ( tr.fraction != 1.0 && pEntity == tr.m_pEnt )
+ {
+ ClearMultiDamage( );
+ pEntity->DispatchTraceAttack( adjustedInfo, dir, &tr );
+ ApplyMultiDamage();
+ }
+ else
+ {
+ nDamageTaken = pEntity->TakeDamage( adjustedInfo );
+ }
+
+ // Now hit all triggers along the way that respond to damage.
+ pEntity->TraceAttackToTriggers( adjustedInfo, vecSrc, tr.endpos, dir );
+ return nDamageTaken;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : &info -
+// &vecSrcIn -
+// flRadius -
+// iClassIgnore -
+// *pEntityIgnore -
+//-----------------------------------------------------------------------------
+void CTFGameRules::RadiusDamage( const CTakeDamageInfo &info, const Vector &vecSrcIn, float flRadius, int iClassIgnore, CBaseEntity *pEntityIgnore )
+{
+ // This shouldn't be used. Call the version above that takes a CTFRadiusDamageInfo pointer.
+ Assert(0);
+
+ CTakeDamageInfo dmgInfo = info;
+ Vector vecSrc = vecSrcIn;
+ CTFRadiusDamageInfo radiusinfo( &dmgInfo, vecSrc, flRadius, pEntityIgnore );
+ RadiusDamage(radiusinfo);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ApplyOnDamageModifyRules( CTakeDamageInfo &info, CBaseEntity *pVictimBaseEntity, bool bAllowDamage )
+{
+ info.SetDamageForForceCalc( info.GetDamage() );
+ bool bDebug = tf_debug_damage.GetBool();
+
+ CTFPlayer *pVictim = ToTFPlayer( pVictimBaseEntity );
+ CBaseEntity *pAttacker = info.GetAttacker();
+ CTFPlayer *pTFAttacker = ToTFPlayer( pAttacker );
+
+ // 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 * >( info.GetWeapon() );
+
+ bool bShowDisguisedCrit = false;
+ bool bAllSeeCrit = false;
+ EAttackBonusEffects_t eBonusEffect = kBonusEffect_None;
+
+ if ( pVictim )
+ {
+ pVictim->SetSeeCrit( false, false, false );
+ pVictim->SetAttackBonusEffect( kBonusEffect_None );
+ }
+
+ int bitsDamage = info.GetDamageType();
+
+ // Damage type was already crit (Flares / headshot)
+ if ( bitsDamage & DMG_CRITICAL )
+ {
+ info.SetCritType( CTakeDamageInfo::CRIT_FULL );
+ }
+
+ // First figure out whether this is going to be a full forced crit for some specific reason. It's
+ // important that we do this before figuring out whether we're going to be a minicrit or not.
+
+ // Allow attributes to force critical hits on players with specific conditions
+ if ( pVictim )
+ {
+ // Crit against players that have these conditions
+ int iCritDamageTypes = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iCritDamageTypes, or_crit_vs_playercond );
+
+ if ( iCritDamageTypes )
+ {
+ // iCritDamageTypes is an or'd list of types. We need to pull each bit out and
+ // then test against what that bit in the items_master file maps to.
+ for ( int i = 0; condition_to_attribute_translation[i] != TF_COND_LAST; i++ )
+ {
+ if ( iCritDamageTypes & ( 1 << i ) )
+ {
+ if ( pVictim->m_Shared.InCond( condition_to_attribute_translation[ i ] ) )
+ {
+ bitsDamage |= DMG_CRITICAL;
+ info.AddDamageType( DMG_CRITICAL );
+ info.SetCritType( CTakeDamageInfo::CRIT_FULL );
+
+ if ( condition_to_attribute_translation[i] == TF_COND_DISGUISED ||
+ condition_to_attribute_translation[i] == TF_COND_DISGUISING )
+ {
+ // if our attribute specifically crits disguised enemies we need to show it on the client
+ bShowDisguisedCrit = true;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ int iCritVsWet = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iCritVsWet, crit_vs_wet_players );
+ if ( iCritVsWet )
+ {
+ float flWaterExitTime = pVictim->GetWaterExitTime();
+
+ if ( pVictim->m_Shared.InCond( TF_COND_URINE ) ||
+ pVictim->m_Shared.InCond( TF_COND_MAD_MILK ) ||
+ ( pVictim->GetWaterLevel() > WL_NotInWater ) ||
+ ( ( flWaterExitTime > 0 ) && ( gpGlobals->curtime - flWaterExitTime < 5.0f ) ) ) // or they exited the water in the last few seconds
+ {
+ bitsDamage |= DMG_CRITICAL;
+ info.AddDamageType( DMG_CRITICAL );
+ info.SetCritType( CTakeDamageInfo::CRIT_FULL );
+ }
+ }
+
+ // Crit against players that don't have these conditions
+ int iCritDamageNotTypes = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iCritDamageNotTypes, or_crit_vs_not_playercond );
+
+ if ( iCritDamageNotTypes )
+ {
+ // iCritDamageTypes is an or'd list of types. We need to pull each bit out and
+ // then test against what that bit in the items_master file maps to.
+ for ( int i = 0; condition_to_attribute_translation[i] != TF_COND_LAST; i++ )
+ {
+ if ( iCritDamageNotTypes & ( 1 << i ) )
+ {
+ if ( !pVictim->m_Shared.InCond( condition_to_attribute_translation[ i ] ) )
+ {
+ bitsDamage |= DMG_CRITICAL;
+ info.AddDamageType( DMG_CRITICAL );
+ info.SetCritType( CTakeDamageInfo::CRIT_FULL );
+
+ if ( condition_to_attribute_translation[ i ] == TF_COND_DISGUISED ||
+ condition_to_attribute_translation[ i ] == TF_COND_DISGUISING )
+ {
+ // if our attribute specifically crits disguised enemies we need to show it on the client
+ bShowDisguisedCrit = true;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ // Crit burning behind
+ int iCritBurning = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iCritBurning, axtinguisher_properties );
+ if ( iCritBurning && pVictim->m_Shared.InCond( TF_COND_BURNING ) )
+ {
+ // Full crit in back, mini in front
+ Vector toEnt = pVictim->GetAbsOrigin() - pTFAttacker->GetAbsOrigin();
+ {
+ Vector entForward;
+ AngleVectors( pVictim->EyeAngles(), &entForward );
+ toEnt.z = 0;
+ toEnt.NormalizeInPlace();
+
+ if ( DotProduct( toEnt, entForward ) > 0.0f ) // 90 degrees from center (total of 180)
+ {
+ bitsDamage |= DMG_CRITICAL;
+ info.AddDamageType( DMG_CRITICAL );
+ info.SetCritType( CTakeDamageInfo::CRIT_FULL );
+ }
+ else
+ {
+ bAllSeeCrit = true;
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+ }
+ }
+ }
+ }
+
+ int iCritWhileAirborne = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iCritWhileAirborne, crit_while_airborne );
+ if ( iCritWhileAirborne && pTFAttacker )
+ {
+ if ( pTFAttacker->InAirDueToExplosion() )
+ {
+ bitsDamage |= DMG_CRITICAL;
+ info.AddDamageType( DMG_CRITICAL );
+ info.SetCritType( CTakeDamageInfo::CRIT_FULL );
+ }
+ }
+
+ // For awarding assist damage stat later
+ ETFCond eDamageBonusCond = TF_COND_LAST;
+
+ // Some forms of damage override long range damage falloff
+ bool bIgnoreLongRangeDmgEffects = false;
+
+ // Figure out if it's a minicrit or not
+ // But we never minicrit ourselves.
+ if ( pAttacker != pVictimBaseEntity )
+ {
+ if ( info.GetCritType() == CTakeDamageInfo::CRIT_NONE )
+ {
+ CBaseEntity *pInflictor = info.GetInflictor();
+ CTFGrenadePipebombProjectile *pBaseGrenade = dynamic_cast< CTFGrenadePipebombProjectile* >( pInflictor );
+ CTFBaseRocket *pBaseRocket = dynamic_cast< CTFBaseRocket* >( pInflictor );
+
+ if ( pVictim && ( pVictim->m_Shared.InCond( TF_COND_URINE ) ||
+ pVictim->m_Shared.InCond( TF_COND_MARKEDFORDEATH ) ||
+ pVictim->m_Shared.InCond( TF_COND_MARKEDFORDEATH_SILENT ) ||
+ pVictim->m_Shared.InCond( TF_COND_PASSTIME_PENALTY_DEBUFF ) ) )
+ {
+ bAllSeeCrit = true;
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+
+ if ( !pVictim->m_Shared.InCond( TF_COND_MARKEDFORDEATH_SILENT ) )
+ {
+ eDamageBonusCond = pVictim->m_Shared.InCond( TF_COND_URINE ) ? TF_COND_URINE : TF_COND_MARKEDFORDEATH;
+ }
+ }
+ else if ( pTFAttacker && ( pTFAttacker->m_Shared.InCond( TF_COND_OFFENSEBUFF ) || pTFAttacker->m_Shared.InCond( TF_COND_NOHEALINGDAMAGEBUFF ) ) )
+ {
+ // Attackers buffed by the soldier do mini-crits.
+ bAllSeeCrit = true;
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+
+ if ( pTFAttacker->m_Shared.InCond( TF_COND_OFFENSEBUFF ) )
+ {
+ eDamageBonusCond = TF_COND_OFFENSEBUFF;
+ }
+ }
+ else if ( pTFAttacker && (bitsDamage & DMG_RADIUS_MAX) && pWeapon && ( (pWeapon->GetWeaponID() == TF_WEAPON_SWORD) || (pWeapon->GetWeaponID() == TF_WEAPON_BOTTLE)|| (pWeapon->GetWeaponID() == TF_WEAPON_WRENCH) ) )
+ {
+ // First sword or bottle attack after a charge is a mini-crit.
+ bAllSeeCrit = true;
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+ }
+ else if ( ( pInflictor && pInflictor->IsPlayer() == false ) && ( ( pBaseRocket && pBaseRocket->GetDeflected() ) || ( pBaseGrenade && pBaseGrenade->GetDeflected() && ( pBaseGrenade->ShouldMiniCritOnReflect() ) ) ) )
+ {
+ // Reflected rockets, grenades (non-remote detonate), arrows always mini-crit
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_PLASMA_CHARGED )
+ {
+ // Charged plasma shots do minicrits.
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_CLEAVER_CRIT )
+ {
+ // Long range cleaver hit
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+ }
+ else if ( pTFAttacker && ( pTFAttacker->m_Shared.InCond( TF_COND_ENERGY_BUFF ) ) )
+ {
+ // Scouts using crit drink do mini-crits, as well as receive them
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+ }
+ else if ( ( info.GetDamageType() & DMG_IGNITE ) && pVictim && pVictim->m_Shared.InCond( TF_COND_BURNING ) && info.GetDamageCustom() == TF_DMG_CUSTOM_BURNING_FLARE )
+ {
+ CTFFlareGun *pFlareGun = dynamic_cast< CTFFlareGun* >( pWeapon );
+ if ( pFlareGun && pFlareGun->GetFlareGunType() != FLAREGUN_GRORDBORT )
+ {
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+ }
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_FLARE_PELLET )
+ {
+ CBaseEntity *pInflictor = info.GetInflictor();
+ CTFProjectile_Flare *pFlare = dynamic_cast< CTFProjectile_Flare* >( pInflictor );
+ if ( pFlare && pFlare->IsFromTaunt() && pFlare->GetTimeAlive() < 0.05f )
+ {
+ // Taunt crits fired from the scorch shot at short range are super powerful!
+ info.SetDamage( 400.0f );
+ }
+ }
+ else if( pTFAttacker && pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_CANNON && ( info.GetDamageType() & DMG_BLAST ) )
+ {
+ CTFGrenadeLauncher* pGrenadeLauncher = static_cast<CTFGrenadeLauncher*>( pWeapon );
+ if( pGrenadeLauncher->IsDoubleDonk( pVictim ) )
+ {
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_DoubleDonk;
+ info.SetDamage( info.GetMaxDamage() ); // Double donk victims score max damage
+ EconEntity_OnOwnerKillEaterEvent( pGrenadeLauncher, pTFAttacker, pVictim, kKillEaterEvent_DoubleDonks );
+ }
+ }
+ else
+ {
+ // Allow Attributes to shortcut out if found, no need to check all of them
+ for ( int i = 0; i < 1; ++i )
+ {
+ // Some weapons force minicrits on burning targets.
+ // Does not work for burn but works for ignite
+ int iForceMiniCritOnBurning = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iForceMiniCritOnBurning, or_minicrit_vs_playercond_burning );
+ if ( iForceMiniCritOnBurning == 1 && pVictim && pVictim->m_Shared.InCond( TF_COND_BURNING ) && !( info.GetDamageType() & DMG_BURN ) )
+ {
+ bAllSeeCrit = true;
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+ break;
+ }
+
+ // Some weapons mini-crit airborne targets. Airborne targets are any target that has been knocked
+ // into the air by an explosive force from an enemy.
+ int iMiniCritAirborne = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iMiniCritAirborne, mini_crit_airborne );
+ if ( iMiniCritAirborne == 1 && pVictim && pVictim->InAirDueToKnockback() )
+ {
+ bAllSeeCrit = true;
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+ break;
+ }
+
+ //// Some weapons minicrit *any* target in the air, regardless of how they got there.
+ //int iMiniCritAirborneDeploy = 0;
+ //CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iMiniCritAirborneDeploy, mini_crit_airborne_deploy );
+ //if ( iMiniCritAirborneDeploy > 0 &&
+ // pWeapon &&
+ // ( gpGlobals->curtime - pWeapon->GetLastDeployTime() ) < iMiniCritAirborneDeploy &&
+ // //
+
+ // pVictim && !( pVictim->GetFlags() & FL_ONGROUND ) &&
+ // ( pVictim->GetWaterLevel() == WL_NotInWater ) )
+ //{
+ // bAllSeeCrit = true;
+ // info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ // eBonusEffect = kBonusEffect_MiniCrit;
+ // break;
+ //}
+
+ if ( pTFAttacker && pVictim )
+ {
+ // MiniCrit a victims back at close range
+ int iMiniCritBackAttack = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iMiniCritBackAttack, closerange_backattack_minicrits );
+ Vector toEnt = pVictim->GetAbsOrigin() - pTFAttacker->GetAbsOrigin();
+ if ( iMiniCritBackAttack == 1 && toEnt.LengthSqr() < Square( 512.0f ) )
+ {
+ Vector entForward;
+ AngleVectors( pVictim->EyeAngles(), &entForward );
+ toEnt.z = 0;
+ toEnt.NormalizeInPlace();
+
+ if ( DotProduct( toEnt, entForward ) > 0.259f ) // 75 degrees from center (total of 150)
+ {
+ bAllSeeCrit = true;
+ info.SetCritType( CTakeDamageInfo::CRIT_MINI );
+ eBonusEffect = kBonusEffect_MiniCrit;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Don't do long range distance falloff when pAttacker has Rocket Specialist attrib and directly hits an enemy
+ if ( pBaseRocket && pBaseRocket->GetEnemy() && pBaseRocket->GetEnemy() == pVictimBaseEntity )
+ {
+ int iRocketSpecialist = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pAttacker, iRocketSpecialist, rocket_specialist );
+ if ( iRocketSpecialist )
+ bIgnoreLongRangeDmgEffects = true;
+ }
+
+ // Some Powerups remove distance damage falloff
+ if ( pTFAttacker && ( pTFAttacker->m_Shared.GetCarryingRuneType() == RUNE_STRENGTH || pTFAttacker->m_Shared.GetCarryingRuneType() == RUNE_PRECISION ) )
+ {
+ bIgnoreLongRangeDmgEffects = true;
+ }
+ }
+ }
+
+ if ( info.GetCritType() == CTakeDamageInfo::CRIT_MINI )
+ {
+ int iPromoteMiniCritToCrit = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iPromoteMiniCritToCrit, minicrits_become_crits );
+ if ( iPromoteMiniCritToCrit == 1 )
+ {
+ info.SetCritType( CTakeDamageInfo::CRIT_FULL );
+ eBonusEffect = kBonusEffect_Crit;
+ bitsDamage |= DMG_CRITICAL;
+ info.AddDamageType( DMG_CRITICAL );
+ }
+ }
+
+ if ( pVictim )
+ {
+ pVictim->SetSeeCrit( bAllSeeCrit, info.GetCritType() == CTakeDamageInfo::CRIT_MINI, bShowDisguisedCrit );
+ pVictim->SetAttackBonusEffect( eBonusEffect );
+ }
+
+ // If we're invulnerable, force ourselves to only take damage events only, so we still get pushed
+ if ( pVictim && pVictim->m_Shared.IsInvulnerable() )
+ {
+ if ( !bAllowDamage )
+ {
+ int iOldTakeDamage = pVictim->m_takedamage;
+ pVictim->m_takedamage = DAMAGE_EVENTS_ONLY;
+ // NOTE: Deliberately skip base player OnTakeDamage, because we don't want all the stuff it does re: suit voice
+ pVictim->CBaseCombatCharacter::OnTakeDamage( info );
+ pVictim->m_takedamage = iOldTakeDamage;
+
+ // Burn sounds are handled in ConditionThink()
+ if ( !(bitsDamage & DMG_BURN ) )
+ {
+ pVictim->SpeakConceptIfAllowed( MP_CONCEPT_HURT );
+ }
+
+ // If this is critical explosive damage, and the Medic giving us invuln triggered
+ // it in the last second, he's earned himself an achievement.
+ if ( (bitsDamage & DMG_CRITICAL) && (bitsDamage & DMG_BLAST) )
+ {
+ pVictim->m_Shared.CheckForAchievement( ACHIEVEMENT_TF_MEDIC_SAVE_TEAMMATE );
+ }
+
+ return false;
+ }
+ }
+
+
+ if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BACKSTAB )
+ {
+ // Jarate backstabber
+ int iJarateBackstabber = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pVictim, iJarateBackstabber, jarate_backstabber );
+ if ( iJarateBackstabber > 0 && pTFAttacker )
+ {
+ pTFAttacker->m_Shared.AddCond( TF_COND_URINE, 10.0f, pVictim );
+ pTFAttacker->m_Shared.SetPeeAttacker( pVictim );
+ pTFAttacker->SpeakConceptIfAllowed( MP_CONCEPT_JARATE_HIT );
+ }
+
+ if ( pVictim && pVictim->CheckBlockBackstab( pTFAttacker ) )
+ {
+ // The backstab was absorbed by a shield.
+ info.SetDamage( 0 );
+
+ // Shake nearby players' screens.
+ UTIL_ScreenShake( pVictim->GetAbsOrigin(), 25.f, 150.0, 1.0, 50.f, SHAKE_START );
+
+ // Play the notification sound.
+ pVictim->EmitSound( "Player.Spy_Shield_Break" );
+
+ // Unzoom the sniper.
+ CTFWeaponBase *pWeapon = pVictim->GetActiveTFWeapon();
+ if ( pWeapon && WeaponID_IsSniperRifle( pWeapon->GetWeaponID() ) )
+ {
+ CTFSniperRifle *pSniperRifle = static_cast< CTFSniperRifle* >( pWeapon );
+ if ( pSniperRifle->IsZoomed() )
+ {
+ pSniperRifle->ZoomOut();
+ }
+ }
+
+ // Vibrate the spy's knife.
+ if ( pTFAttacker && pTFAttacker->GetActiveWeapon() )
+ {
+ CTFKnife *pKnife = (CTFKnife *) pTFAttacker->GetActiveWeapon();
+ if ( pKnife )
+ {
+ pKnife->BackstabBlocked();
+ }
+ }
+
+ // Tell the clients involved in the jarate
+ CRecipientFilter involved_filter;
+ involved_filter.AddRecipient( pVictim );
+ involved_filter.AddRecipient( pTFAttacker );
+
+ UserMessageBegin( involved_filter, "PlayerShieldBlocked" );
+ WRITE_BYTE( pTFAttacker->entindex() );
+ WRITE_BYTE( pVictim->entindex() );
+ MessageEnd();
+ }
+ }
+
+ // Apply attributes that increase damage vs players
+ if ( pWeapon )
+ {
+ float flDamage = info.GetDamage();
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pWeapon, flDamage, mult_dmg_vs_players );
+
+ // Check if we're to boost damage against the same class
+ if( pVictim && pTFAttacker )
+ {
+ int nVictimClass = pVictim->GetPlayerClass()->GetClassIndex();
+ int nAttackerClass = pTFAttacker->GetPlayerClass()->GetClassIndex();
+
+ // Same class?
+ if( nVictimClass == nAttackerClass )
+ {
+ // Potentially boost damage
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pWeapon, flDamage, mult_dmg_vs_same_class );
+ }
+ }
+
+ info.SetDamage( flDamage );
+ }
+
+ if ( pVictim && !pVictim->m_Shared.InCond( TF_COND_BURNING ) )
+ {
+ if ( bitsDamage & DMG_CRITICAL )
+ {
+ if ( pTFAttacker && !pTFAttacker->m_Shared.IsCritBoosted() )
+ {
+ int iNonBurningCritsDisabled = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iNonBurningCritsDisabled, set_nocrit_vs_nonburning );
+ if ( iNonBurningCritsDisabled )
+ {
+ bitsDamage &= ~DMG_CRITICAL;
+ info.SetDamageType( info.GetDamageType() & (~DMG_CRITICAL) );
+ info.SetCritType( CTakeDamageInfo::CRIT_NONE );
+ }
+ }
+ }
+
+ float flDamage = info.GetDamage();
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pWeapon, flDamage, mult_dmg_vs_nonburning );
+ info.SetDamage( flDamage );
+ }
+
+ // Alien Isolation SetBonus Checking
+ if ( pVictim && pTFAttacker && pWeapon )
+ {
+ // Alien->Merc melee bonus
+ if ( ( info.GetDamageType() & (DMG_CLUB|DMG_SLASH) ) && info.GetDamageCustom() != TF_DMG_CUSTOM_BASEBALL )
+ {
+ CTFWeaponBaseMelee *pMelee = dynamic_cast<CTFWeaponBaseMelee*>( pWeapon );
+ if ( pMelee )
+ {
+ int iAttackerAlien = 0;
+ int iVictimMerc = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pTFAttacker, iAttackerAlien, alien_isolation_xeno_bonus_pos );
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pVictim, iVictimMerc, alien_isolation_merc_bonus_pos );
+
+ if ( iAttackerAlien && iVictimMerc )
+ {
+ info.SetDamage( info.GetDamage() * 5.0f );
+ }
+ }
+ }
+
+ // Merc->Alien MK50 damage, aka flamethrower
+ if ( ( info.GetDamageType() & DMG_IGNITE ) && pWeapon->GetWeaponID() == TF_WEAPON_FLAMETHROWER )
+ {
+ int iAttackerMerc = 0;
+ int iVictimAlien = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pTFAttacker, iAttackerMerc, alien_isolation_merc_bonus_pos );
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pVictim, iVictimAlien, alien_isolation_xeno_bonus_pos );
+
+ if ( iAttackerMerc && iVictimAlien )
+ {
+ info.SetDamage( info.GetDamage() * 3.0f );
+ }
+ }
+ }
+
+ int iPierceResists = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iPierceResists, mod_ignore_resists_absorbs );
+
+ // Use defense buffs if it's not a backstab or direct crush damage (telefrage, etc.)
+ if ( pVictim && info.GetDamageCustom() != TF_DMG_CUSTOM_BACKSTAB && ( info.GetDamageType() & DMG_CRUSH ) == 0 )
+ {
+ if ( pVictim->m_Shared.InCond( TF_COND_DEFENSEBUFF ) )
+ {
+ // We take no crits of any kind...
+ if( eBonusEffect == kBonusEffect_MiniCrit || eBonusEffect == kBonusEffect_Crit )
+ eBonusEffect = kBonusEffect_None;
+ info.SetCritType( CTakeDamageInfo::CRIT_NONE );
+ bAllSeeCrit = false;
+ bShowDisguisedCrit = false;
+
+ pVictim->SetSeeCrit( bAllSeeCrit, false, bShowDisguisedCrit );
+ pVictim->SetAttackBonusEffect( eBonusEffect );
+
+ bitsDamage &= ~DMG_CRITICAL;
+ info.SetDamageType( bitsDamage );
+ info.SetCritType( CTakeDamageInfo::CRIT_NONE );
+ }
+
+ if ( !iPierceResists )
+ {
+ // If we are defense buffed...
+ if ( pVictim->m_Shared.InCond( TF_COND_DEFENSEBUFF_HIGH ) )
+ {
+ // We take 75% less damage... still take crits
+ info.SetDamage( info.GetDamage() * 0.25f );
+ }
+ else if ( pVictim->m_Shared.InCond( TF_COND_DEFENSEBUFF ) || pVictim->m_Shared.InCond( TF_COND_DEFENSEBUFF_NO_CRIT_BLOCK ) )
+ {
+ // defense buffs gives 50% to sentry dmg and 35% from all other sources
+ CObjectSentrygun *pSentry = ( info.GetInflictor() && info.GetInflictor()->IsBaseObject() ) ? dynamic_cast< CObjectSentrygun* >( info.GetInflictor() ) : NULL;
+ if ( pSentry )
+ {
+ info.SetDamage( info.GetDamage() * 0.50f );
+ }
+ else
+ {
+ // And we take 35% less damage...
+ info.SetDamage( info.GetDamage() * 0.65f );
+ }
+ }
+ }
+ }
+
+ // A note about why crits now go through the randomness/variance code:
+ // Normally critical damage is not affected by variance. However, we always want to measure what that variance
+ // would have been so that we can lump it into the DamageBonus value inside the info. This means crits actually
+ // boost more than 3X when you factor the reduction we avoided. Example: a rocket that normally would do 50
+ // damage due to range now does the original 100, which is then multiplied by 3, resulting in a 6x increase.
+ bool bCrit = ( bitsDamage & DMG_CRITICAL ) ? true : false;
+
+ // If we're not damaging ourselves, apply randomness
+ if ( pAttacker != pVictimBaseEntity && !(bitsDamage & (DMG_DROWN | DMG_FALL)) )
+ {
+ float flDamage = info.GetDamage();
+ float flDmgVariance = 0.f;
+
+ // Minicrits still get short range damage bonus
+ bool bForceCritFalloff = ( bitsDamage & DMG_USEDISTANCEMOD ) &&
+ ( ( bCrit && tf_weapon_criticals_distance_falloff.GetBool() ) ||
+ ( info.GetCritType() == CTakeDamageInfo::CRIT_MINI && tf_weapon_minicrits_distance_falloff.GetBool() ) );
+ bool bDoShortRangeDistanceIncrease = !bCrit || info.GetCritType() == CTakeDamageInfo::CRIT_MINI ;
+ bool bDoLongRangeDistanceDecrease = !bIgnoreLongRangeDmgEffects && ( bForceCritFalloff || ( !bCrit && info.GetCritType() != CTakeDamageInfo::CRIT_MINI ) );
+
+ // If we're doing any distance modification, we need to do that first
+ float flRandomDamage = info.GetDamage() * tf_damage_range.GetFloat();
+
+ float flRandomDamageSpread = 0.10f;
+ float flMin = 0.5 - flRandomDamageSpread;
+ float flMax = 0.5 + flRandomDamageSpread;
+
+ if ( bitsDamage & DMG_USEDISTANCEMOD )
+ {
+ Vector vAttackerPos = pAttacker->WorldSpaceCenter();
+ float flOptimalDistance = 512.0;
+
+ // Use Sentry position for distance mod
+ CObjectSentrygun *pSentry = dynamic_cast<CObjectSentrygun*>( info.GetInflictor() );
+ if ( pSentry )
+ {
+ vAttackerPos = pSentry->WorldSpaceCenter();
+ // Sentries have a much further optimal distance
+ flOptimalDistance = SENTRY_MAX_RANGE;
+ }
+ // The base sniper rifle doesn't have DMG_USEDISTANCEMOD, so this isn't used. Unlockable rifle had it for a bit.
+ else if ( pWeapon && WeaponID_IsSniperRifle( pWeapon->GetWeaponID() ) )
+ {
+ flOptimalDistance *= 2.5f;
+ }
+
+ float flDistance = MAX( 1.0, ( pVictimBaseEntity->WorldSpaceCenter() - vAttackerPos).Length() );
+
+ float flCenter = RemapValClamped( flDistance / flOptimalDistance, 0.0, 2.0, 1.0, 0.0 );
+ if ( ( flCenter > 0.5 && bDoShortRangeDistanceIncrease ) || flCenter <= 0.5 )
+ {
+ if ( bitsDamage & DMG_NOCLOSEDISTANCEMOD )
+ {
+ if ( flCenter > 0.5 )
+ {
+ // Reduce the damage bonus at close range
+ flCenter = RemapVal( flCenter, 0.5, 1.0, 0.5, 0.65 );
+ }
+ }
+ flMin = MAX( 0.0, flCenter - flRandomDamageSpread );
+ flMax = MIN( 1.0, flCenter + flRandomDamageSpread );
+
+ if ( bDebug )
+ {
+ Warning(" RANDOM: Dist %.2f, Ctr: %.2f, Min: %.2f, Max: %.2f\n", flDistance, flCenter, flMin, flMax );
+ }
+ }
+ else
+ {
+ if ( bDebug )
+ {
+ Warning(" NO DISTANCE MOD: Dist %.2f, Ctr: %.2f, Min: %.2f, Max: %.2f\n", flDistance, flCenter, flMin, flMax );
+ }
+ }
+ }
+ //Msg("Range: %.2f - %.2f\n", flMin, flMax );
+ float flRandomRangeVal;
+ if ( tf_damage_disablespread.GetBool() || ( pTFAttacker && pTFAttacker->m_Shared.GetCarryingRuneType() == RUNE_PRECISION ) )
+ {
+ flRandomRangeVal = flMin + flRandomDamageSpread;
+ }
+ else
+ {
+ flRandomRangeVal = RandomFloat( flMin, flMax );
+ }
+
+ //if ( bDebug )
+ //{
+ // Warning( " Val: %.2f\n", flRandomRangeVal );
+ //}
+
+ // Weapon Based Damage Mod
+ if ( pWeapon && pAttacker && pAttacker->IsPlayer() )
+ {
+ switch ( pWeapon->GetWeaponID() )
+ {
+ // Rocket launcher only has half the bonus of the other weapons at short range
+ // Rocket Launchers
+ case TF_WEAPON_ROCKETLAUNCHER :
+ case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT :
+ case TF_WEAPON_PARTICLE_CANNON :
+ if ( flRandomRangeVal > 0.5 )
+ {
+ flRandomDamage *= 0.5f;
+ }
+ break;
+ case TF_WEAPON_PIPEBOMBLAUNCHER : // Stickies
+ case TF_WEAPON_GRENADELAUNCHER :
+ case TF_WEAPON_CANNON :
+ case TF_WEAPON_STICKBOMB:
+ if ( !( bitsDamage & DMG_NOCLOSEDISTANCEMOD ) )
+ {
+ flRandomDamage *= 0.2f;
+ }
+ break;
+ case TF_WEAPON_SCATTERGUN :
+ case TF_WEAPON_SODA_POPPER :
+ case TF_WEAPON_PEP_BRAWLER_BLASTER :
+ //case TF_WEAPON_HANDGUN_SCOUT_PRIMARY : // Shortstop
+ // Scattergun gets 50% bonus at short range
+ if ( flRandomRangeVal > 0.5 )
+ {
+ flRandomDamage *= 1.5f;
+ }
+ break;
+ }
+ }
+
+ // Random damage variance.
+ flDmgVariance = SimpleSplineRemapValClamped( flRandomRangeVal, 0, 1, -flRandomDamage, flRandomDamage );
+ if ( ( bDoShortRangeDistanceIncrease && flDmgVariance > 0.f ) || bDoLongRangeDistanceDecrease )
+ {
+ flDamage = info.GetDamage() + flDmgVariance;
+ }
+
+ if ( bDebug )
+ {
+ Warning(" Out: %.2f -> Final %.2f\n", flDmgVariance, flDamage );
+ }
+
+ /*
+ for ( float flVal = flMin; flVal <= flMax; flVal += 0.05 )
+ {
+ float flOut = SimpleSplineRemapValClamped( flVal, 0, 1, -flRandomDamage, flRandomDamage );
+ Msg("Val: %.2f, Out: %.2f, Dmg: %.2f\n", flVal, flOut, info.GetDamage() + flOut );
+ }
+ */
+
+ // Burn sounds are handled in ConditionThink()
+ if ( !(bitsDamage & DMG_BURN ) && pVictim )
+ {
+ pVictim->SpeakConceptIfAllowed( MP_CONCEPT_HURT );
+ }
+
+
+ // Save any bonus damage as a separate value
+ float flCritDamage = 0.f;
+ // Yes, it's weird that we sometimes fabs flDmgVariance. Here's why: In the case of a crit rocket, we
+ // know that number will generally be negative due to dist or randomness. In this case, we want to track
+ // that effect - even if we don't apply it. In the case of our crit rocket that normally would lose 50
+ // damage, we fabs'd so that we can account for it as a bonus - since it's present in a crit.
+ float flBonusDamage = bForceCritFalloff ? 0.f : fabs( flDmgVariance );
+ CTFPlayer *pProvider = NULL;
+
+ if ( info.GetCritType() == CTakeDamageInfo::CRIT_MINI )
+ {
+ // We should never have both of these flags set or Weird Things will happen with the damage numbers
+ // that aren't clear to the players. Or us, really.
+ Assert( !(bitsDamage & DMG_CRITICAL) );
+
+ if ( bDebug )
+ {
+ Warning( " MINICRIT: Dmg %.2f -> ", flDamage );
+ }
+
+ COMPILE_TIME_ASSERT( TF_DAMAGE_MINICRIT_MULTIPLIER > 1.f );
+ flCritDamage = ( TF_DAMAGE_MINICRIT_MULTIPLIER - 1.f ) * flDamage;
+
+ bitsDamage |= DMG_CRITICAL;
+ info.AddDamageType( DMG_CRITICAL );
+
+ // Any condition assist stats to send out?
+ if ( eDamageBonusCond < TF_COND_LAST )
+ {
+ if ( pVictim )
+ {
+ pProvider = ToTFPlayer( pVictim->m_Shared.GetConditionProvider( eDamageBonusCond ) );
+ if ( pProvider )
+ {
+ CTF_GameStats.Event_PlayerDamageAssist( pProvider, flCritDamage + flBonusDamage );
+ }
+ }
+ if ( pTFAttacker )
+ {
+ pProvider = ToTFPlayer( pTFAttacker->m_Shared.GetConditionProvider( eDamageBonusCond ) );
+ if ( pProvider && pProvider != pTFAttacker )
+ {
+ CTF_GameStats.Event_PlayerDamageAssist( pProvider, flCritDamage + flBonusDamage );
+ }
+ }
+ }
+
+ if ( bDebug )
+ {
+ Warning( "reduced to %.2f before crit mult\n", flDamage );
+ }
+ }
+
+ if ( bCrit )
+ {
+ if ( info.GetCritType() != CTakeDamageInfo::CRIT_MINI )
+ {
+ COMPILE_TIME_ASSERT( TF_DAMAGE_CRIT_MULTIPLIER > 1.f );
+ flCritDamage = ( TF_DAMAGE_CRIT_MULTIPLIER - 1.f ) * flDamage;
+ }
+
+ if ( bDebug )
+ {
+ Warning( " CRITICAL! Damage: %.2f\n", flDamage );
+ }
+
+ // Burn sounds are handled in ConditionThink()
+ if ( !(bitsDamage & DMG_BURN ) && pVictim )
+ {
+ pVictim->SpeakConceptIfAllowed( MP_CONCEPT_HURT, "damagecritical:1" );
+ }
+
+ if ( pTFAttacker && pTFAttacker->m_Shared.IsCritBoosted() )
+ {
+ pProvider = ToTFPlayer( pTFAttacker->m_Shared.GetConditionProvider( TF_COND_CRITBOOSTED ) );
+ if ( pProvider && pTFAttacker && pProvider != pTFAttacker )
+ {
+ CTF_GameStats.Event_PlayerDamageAssist( pProvider, flCritDamage + flBonusDamage );
+ }
+ }
+ }
+
+ if ( pAttacker && pAttacker->IsPlayer() )
+ {
+ // Modify damage based on bonuses
+ flDamage *= pTFAttacker->m_Shared.GetTmpDamageBonus();
+ }
+
+ // Store the extra damage and update actual damage
+ if ( bCrit || info.GetCritType() == CTakeDamageInfo::CRIT_MINI )
+ {
+ info.SetDamageBonus( flCritDamage + flBonusDamage, pProvider ); // Order-of-operations sensitive, but fine as long as TF_COND_CRITBOOSTED is last
+ }
+
+ // Crit-A-Cola and Steak Sandwich - only increase normal damage
+ if ( pVictim && pVictim->m_Shared.InCond( TF_COND_ENERGY_BUFF ) && !bCrit && info.GetCritType() != CTakeDamageInfo::CRIT_MINI )
+ {
+ float flDmgMult = 1.f;
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim, flDmgMult, energy_buff_dmg_taken_multiplier );
+ flDamage *= flDmgMult;
+ }
+
+ info.SetDamage( flDamage + flCritDamage );
+ }
+
+ if ( pTFAttacker && pTFAttacker->IsPlayerClass( TF_CLASS_SPY ) )
+ {
+ if ( pTFAttacker->GetActiveWeapon() )
+ {
+ int iAddCloakOnHit = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pTFAttacker->GetActiveWeapon(), iAddCloakOnHit, add_cloak_on_hit );
+ if ( iAddCloakOnHit > 0 )
+ {
+ pTFAttacker->m_Shared.AddToSpyCloakMeter( iAddCloakOnHit, true );
+ }
+ }
+ }
+
+
+ // Apply on-hit attributes
+ if ( pVictim && pAttacker && pAttacker->GetTeam() != pVictim->GetTeam() && pAttacker->IsPlayer() && pWeapon )
+ {
+ pWeapon->ApplyOnHitAttributes( pVictimBaseEntity, pTFAttacker, info );
+ }
+
+ // Give assist points to the provider of any stun on the victim - up to half the damage, based on the amount of stun
+ if ( pVictim && pVictim->m_Shared.InCond( TF_COND_STUNNED ) )
+ {
+ CTFPlayer *pProvider = ToTFPlayer( pVictim->m_Shared.GetConditionProvider( TF_COND_STUNNED ) );
+ if ( pProvider && pTFAttacker && pProvider != pTFAttacker )
+ {
+ float flStunAmount = pVictim->m_Shared.GetAmountStunned( TF_STUN_MOVEMENT );
+ if ( flStunAmount < 1.f && pVictim->m_Shared.IsControlStunned() )
+ flStunAmount = 1.f;
+
+ int nAssistPoints = RemapValClamped( flStunAmount, 0.1f, 1.f, 1, ( info.GetDamage() / 2 ) );
+ if ( nAssistPoints )
+ {
+ CTF_GameStats.Event_PlayerDamageAssist( pProvider, nAssistPoints );
+ }
+ }
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+static bool CheckForDamageTypeImmunity( int nDamageType, CTFPlayer* pVictim, float &flDamageBase, float &flCritBonusDamage )
+{
+ bool bImmune = false;
+ if( nDamageType & (DMG_BURN|DMG_IGNITE) )
+ {
+ bImmune = pVictim->m_Shared.InCond( TF_COND_FIRE_IMMUNE );
+ }
+ else if( nDamageType & (DMG_BULLET|DMG_BUCKSHOT) )
+ {
+ bImmune = pVictim->m_Shared.InCond( TF_COND_BULLET_IMMUNE );
+ }
+ else if( nDamageType & DMG_BLAST )
+ {
+ bImmune = pVictim->m_Shared.InCond( TF_COND_BLAST_IMMUNE );
+ }
+
+ if( bImmune )
+ {
+ flDamageBase = flCritBonusDamage = 0.f;
+
+ IGameEvent* event = gameeventmanager->CreateEvent( "damage_resisted" );
+ if ( event )
+ {
+ event->SetInt( "entindex", pVictim->entindex() );
+ gameeventmanager->FireEvent( event );
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+static bool CheckMedicResist( ETFCond ePassiveCond, ETFCond eDeployedCond, CTFPlayer* pVictim, float flRawDamage, float& flDamageBase, bool bCrit, float& flCritBonusDamage )
+{
+ // Might be a tank or some other object that's getting shot
+ if( !pVictim || !pVictim->IsPlayer() )
+ return false;
+
+ ETFCond eActiveCond;
+ // Be in the condition!
+ if( pVictim->m_Shared.InCond( eDeployedCond ) )
+ {
+ eActiveCond = eDeployedCond;
+ }
+ else if( pVictim->m_Shared.InCond( ePassiveCond ) )
+ {
+ eActiveCond = ePassiveCond;
+ }
+ else
+ {
+ return false;
+ }
+
+ // Get our condition provider
+ CBaseEntity* pProvider = pVictim->m_Shared.GetConditionProvider( eActiveCond );
+ CTFPlayer* pTFProvider = ToTFPlayer( pProvider );
+ Assert( pTFProvider );
+
+ float flDamageScale = 0.f;
+ //float flCritBarDeplete = 0.f;
+ bool bUberResist = false;
+
+ if( pTFProvider )
+ {
+ switch( eActiveCond )
+ {
+ case TF_COND_MEDIGUN_UBER_BULLET_RESIST:
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFProvider, flDamageScale, medigun_bullet_resist_deployed );
+ bUberResist = true;
+// if ( bCrit )
+// {
+// CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFProvider, flCritBarDeplete, medigun_crit_bullet_percent_bar_deplete );
+// }
+ break;
+
+ case TF_COND_MEDIGUN_SMALL_BULLET_RESIST:
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFProvider, flDamageScale, medigun_bullet_resist_passive );
+ break;
+
+ case TF_COND_MEDIGUN_UBER_BLAST_RESIST:
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFProvider, flDamageScale, medigun_blast_resist_deployed );
+ bUberResist = true;
+ //if( bCrit )
+ //{
+ // CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFProvider, flCritBarDeplete, medigun_crit_blast_percent_bar_deplete );
+ //}
+ break;
+
+ case TF_COND_MEDIGUN_SMALL_BLAST_RESIST:
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFProvider, flDamageScale, medigun_blast_resist_passive );
+ break;
+
+ case TF_COND_MEDIGUN_UBER_FIRE_RESIST:
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFProvider, flDamageScale, medigun_fire_resist_deployed );
+ bUberResist = true;
+// if( bCrit )
+// {
+// CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFProvider, flCritBarDeplete, medigun_crit_fire_percent_bar_deplete );
+// }
+ break;
+
+ case TF_COND_MEDIGUN_SMALL_FIRE_RESIST:
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFProvider, flDamageScale, medigun_fire_resist_passive );
+ break;
+ }
+ }
+
+ if ( bUberResist && pVictim->m_Shared.InCond( TF_COND_HEALING_DEBUFF ) )
+ {
+ flDamageScale *= 0.75f;
+ }
+
+ flDamageScale = 1.f - flDamageScale;
+ if( flDamageScale < 0.f )
+ flDamageScale = 0.f;
+
+ //// Dont let the medic heal themselves when they take damage
+ //if( pTFProvider && pTFProvider != pVictim )
+ //{
+ // // Heal the medic for 10% of the incoming damage
+ // int nHeal = flRawDamage * 0.10f;
+ // // Heal!
+ // int iHealed = pTFProvider->TakeHealth( nHeal, DMG_GENERIC );
+
+ // // Tell them about it!
+ // if ( iHealed )
+ // {
+ // CTF_GameStats.Event_PlayerHealedOther( pTFProvider, iHealed );
+ // }
+
+ // IGameEvent *event = gameeventmanager->CreateEvent( "player_healonhit" );
+ // if ( event )
+ // {
+ // event->SetInt( "amount", nHeal );
+ // event->SetInt( "entindex", pTFProvider->entindex() );
+ // gameeventmanager->FireEvent( event );
+ // }
+ //}
+
+ IGameEvent* event = gameeventmanager->CreateEvent( "damage_resisted" );
+ if ( event )
+ {
+ event->SetInt( "entindex", pVictim->entindex() );
+ gameeventmanager->FireEvent( event );
+ }
+
+ if ( bCrit && pTFProvider && bUberResist )
+ {
+ flCritBonusDamage = ( pVictim->m_Shared.InCond( TF_COND_HEALING_DEBUFF ) ) ? flCritBonusDamage *= 0.25f : 0.f;
+
+ //CWeaponMedigun* pMedigun = dynamic_cast<CWeaponMedigun*>( pTFProvider->Weapon_OwnsThisID( TF_WEAPON_MEDIGUN ) );
+ //if( pMedigun )
+ //{
+ // if( pMedigun->GetChargeLevel() > 0.f && pMedigun->IsReleasingCharge() )
+ // {
+ // flCritBonusDamage = 0;
+ // }
+ // pMedigun->SubtractCharge( flCritBarDeplete );
+ //}
+ }
+
+ // Scale the damage!
+ flDamageBase = flDamageBase * flDamageScale;
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+static void PotentiallyFireDamageMitigatedEvent( const CTFPlayer* pMitigator, const CTFPlayer* pDamaged, const CEconEntity* pMitigationProvidingEconItem, float flBeforeDamage, float flAfterDamage )
+{
+ int nAmount = flBeforeDamage - flAfterDamage;
+ // Nothing mitigated!
+ if ( nAmount == 0 )
+ return;
+
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "damage_mitigated" );
+ if ( pEvent )
+ {
+ item_definition_index_t nDefIndex = INVALID_ITEM_DEF_INDEX;
+ if ( pMitigationProvidingEconItem && pMitigationProvidingEconItem->GetAttributeContainer() && pMitigationProvidingEconItem->GetAttributeContainer()->GetItem() )
+ {
+ nDefIndex = pMitigationProvidingEconItem->GetAttributeContainer()->GetItem()->GetItemDefinition()->GetDefinitionIndex();
+ }
+
+ pEvent->SetInt( "mitigator", pMitigator ? pMitigator->GetUserID() : -1 );
+ pEvent->SetInt( "damaged", pDamaged ? pDamaged->GetUserID() : -1 );
+ pEvent->SetInt( "amount", nAmount );
+ pEvent->SetInt( "itemdefindex", nDefIndex );
+ gameeventmanager->FireEvent( pEvent, true );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+float CTFGameRules::ApplyOnDamageAliveModifyRules( const CTakeDamageInfo &info, CBaseEntity *pVictimBaseEntity, DamageModifyExtras_t& outParams )
+{
+ CTFPlayer *pVictim = ToTFPlayer( pVictimBaseEntity );
+ CBaseEntity *pAttacker = info.GetAttacker();
+ CTFPlayer *pTFAttacker = ToTFPlayer( pAttacker );
+
+ float flRealDamage = info.GetDamage();
+
+ if ( pVictimBaseEntity && pVictimBaseEntity->m_takedamage != DAMAGE_EVENTS_ONLY && pVictim )
+ {
+ int iDamageTypeBits = info.GetDamageType() & DMG_IGNITE;
+
+ // Handle attributes that want to change our damage type, but only if we're taking damage from a non-DOT. This
+ // stops fire DOT damage from constantly reigniting us. This will also prevent ignites from happening on the
+ // damage *from-a-bleed-DOT*, but not from the bleed application attack.
+ if ( !IsDOTDmg( info.GetDamageCustom() ) )
+ {
+ int iAddBurningDamageType = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iAddBurningDamageType, set_dmgtype_ignite );
+ if ( iAddBurningDamageType )
+ {
+ iDamageTypeBits |= DMG_IGNITE;
+ }
+ }
+
+ // Start burning if we took ignition damage
+ outParams.bIgniting = ( ( iDamageTypeBits & DMG_IGNITE ) && ( !pVictim || pVictim->GetWaterLevel() < WL_Waist ) );
+
+ if ( outParams.bIgniting && pVictim )
+ {
+ if ( pVictim->m_Shared.InCond( TF_COND_DISGUISED ) )
+ {
+ int iDisguiseNoBurn = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pVictim, iDisguiseNoBurn, disguise_no_burn );
+ if ( iDisguiseNoBurn == 1 )
+ {
+ // Do a hard out in the caller
+ return -1;
+ }
+ }
+
+ if ( pVictim->m_Shared.InCond( TF_COND_FIRE_IMMUNE ) )
+ {
+ // Do a hard out in the caller
+ return -1;
+ }
+ }
+
+ // When obscured by smoke, attacks have a chance to miss
+ if ( pVictim && pVictim->m_Shared.InCond( TF_COND_OBSCURED_SMOKE ) )
+ {
+ if ( RandomInt( 1, 4 ) >= 2 )
+ {
+ flRealDamage = 0.f;
+
+ pVictim->SpeakConceptIfAllowed( MP_CONCEPT_DODGE_SHOT );
+
+ if ( pTFAttacker )
+ {
+ CEffectData data;
+ data.m_nHitBox = GetParticleSystemIndex( "miss_text" );
+ data.m_vOrigin = pVictim->WorldSpaceCenter() + Vector( 0.f , 0.f, 32.f );
+ data.m_vAngles = vec3_angle;
+ data.m_nEntIndex = 0;
+
+ CSingleUserRecipientFilter filter( pTFAttacker );
+ te->DispatchEffect( filter, 0.f, data.m_vOrigin, "ParticleEffect", data );
+ }
+
+ // No damage
+ return -1.f;
+ }
+ }
+
+ // Proc invicibility upon being hit
+ float flUberChance = 0.f;
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim, flUberChance, uber_on_damage_taken );
+ if( RandomFloat() < flUberChance )
+ {
+ pVictim->m_Shared.AddCond( TF_COND_INVULNERABLE_CARD_EFFECT, 3.f );
+ // Make sure we don't take any damage
+ flRealDamage = 0.f;
+ }
+
+ // Resists and Boosts
+ float flDamageBonus = info.GetDamageBonus();
+ float flDamageBase = flRealDamage - flDamageBonus;
+ Assert( flDamageBase >= 0.f );
+
+ int iPierceResists = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iPierceResists, mod_pierce_resists_absorbs );
+
+ // This raw damage wont get scaled. Used for determining how much health to give resist medics.
+ float flRawDamage = flDamageBase;
+
+ // Check if we're immune
+ outParams.bPlayDamageReductionSound = CheckForDamageTypeImmunity( info.GetDamageType(), pVictim, flDamageBase, flDamageBonus );
+
+ if ( !iPierceResists )
+ {
+ // Reduce only the crit portion of the damage with crit resist
+ bool bCrit = ( info.GetDamageType() & DMG_CRITICAL ) > 0;
+ if ( bCrit )
+ {
+ // Break the damage down and reassemble
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim, flDamageBonus, mult_dmgtaken_from_crit );
+ }
+
+ // Apply general dmg type reductions. Should we only ever apply one of these? (Flaregun is DMG_BULLET|DMG_IGNITE, for instance)
+ if ( info.GetDamageType() & (DMG_BURN|DMG_IGNITE) )
+ {
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim, flDamageBase, mult_dmgtaken_from_fire );
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim->GetActiveWeapon(), flDamageBase, mult_dmgtaken_from_fire_active );
+
+ // Check for medic resist
+ outParams.bPlayDamageReductionSound = CheckMedicResist( TF_COND_MEDIGUN_SMALL_FIRE_RESIST, TF_COND_MEDIGUN_UBER_FIRE_RESIST, pVictim, flRawDamage, flDamageBase, bCrit, flDamageBonus );
+ }
+
+ if ( pTFAttacker && pVictim && pVictim->m_Shared.InCond( TF_COND_BURNING ) )
+ {
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pTFAttacker->GetActiveWeapon(), flDamageBase, mult_dmg_vs_burning );
+ }
+
+ if ( (info.GetDamageType() & (DMG_BLAST) ) )
+ {
+ bool bReduceBlast = false;
+
+ // If someone else shot us or we're in MvM
+ if( pAttacker != pVictimBaseEntity || IsMannVsMachineMode() )
+ {
+ bReduceBlast = true;
+ }
+
+ if ( bReduceBlast )
+ {
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim, flDamageBase, mult_dmgtaken_from_explosions );
+
+ // Check for medic resist
+ outParams.bPlayDamageReductionSound = CheckMedicResist( TF_COND_MEDIGUN_SMALL_BLAST_RESIST, TF_COND_MEDIGUN_UBER_BLAST_RESIST, pVictim, flRawDamage, flDamageBase, bCrit, flDamageBonus );
+ }
+ }
+
+ if ( info.GetDamageType() & (DMG_BULLET|DMG_BUCKSHOT) )
+ {
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim, flDamageBase, mult_dmgtaken_from_bullets );
+
+ // Check for medic resist
+ outParams.bPlayDamageReductionSound = CheckMedicResist( TF_COND_MEDIGUN_SMALL_BULLET_RESIST, TF_COND_MEDIGUN_UBER_BULLET_RESIST, pVictim, flRawDamage, flDamageBase, bCrit, flDamageBonus );
+ }
+
+ if ( info.GetDamageType() & DMG_MELEE )
+ {
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim, flDamageBase, mult_dmgtaken_from_melee );
+ }
+
+ if ( pVictim->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && pVictim->m_Shared.InCond( TF_COND_AIMING ) && ( ( pVictim->GetHealth() - flRealDamage ) / pVictim->GetMaxHealth() ) <= 0.5f )
+ {
+ float flOriginalDamage = flDamageBase;
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim, flDamageBase, spunup_damage_resistance );
+ if ( flOriginalDamage != flDamageBase )
+ {
+ pVictim->PlayDamageResistSound( flOriginalDamage, flDamageBase );
+ }
+ }
+ }
+
+ // If the damage changed at all play the resist sound
+ if ( flDamageBase != flRawDamage )
+ {
+ outParams.bPlayDamageReductionSound = true;
+ }
+
+ // Stomp flRealDamage with resist adjusted values
+ flRealDamage = flDamageBase + flDamageBonus;
+
+ // Some Powerups apply a damage multiplier. Backstabs are immune to resist protection
+ if ( ( pVictim && info.GetDamageCustom() != TF_DMG_CUSTOM_BACKSTAB ) )
+ {
+ // Plague bleed damage is immune from resist calculation
+ if ( ( !pVictim->m_Shared.InCond( TF_COND_PLAGUE ) && info.GetDamageCustom() != TF_DMG_CUSTOM_BLEEDING ) )
+ {
+ if ( pVictim->m_Shared.GetCarryingRuneType() == RUNE_RESIST )
+ {
+ flRealDamage *= 0.5f;
+ outParams.bPlayDamageReductionSound = true;
+ IGameEvent* event = gameeventmanager->CreateEvent( "damage_resisted" );
+ if ( event )
+ {
+ event->SetInt( "entindex", pVictim->entindex() );
+ gameeventmanager->FireEvent( event );
+ }
+ }
+ else if ( ( pVictim->m_Shared.GetCarryingRuneType() == RUNE_VAMPIRE ) )
+ {
+ flRealDamage *= 0.75f;
+ outParams.bPlayDamageReductionSound = true;
+ }
+ //Plague powerup carrier is resistant to infected enemies
+ else if ( pTFAttacker && ( pVictim->m_Shared.GetCarryingRuneType() == RUNE_PLAGUE ) && pTFAttacker->m_Shared.InCond( TF_COND_PLAGUE ) )
+ {
+ flRealDamage *= 0.5f;
+ outParams.bPlayDamageReductionSound = true;
+ }
+ }
+ }
+
+
+ // End Resists
+
+ // Increased damage taken from all sources
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim, flRealDamage, mult_dmgtaken );
+
+ if ( info.GetInflictor() && info.GetInflictor()->IsBaseObject() )
+ {
+ CObjectSentrygun* pSentry = dynamic_cast<CObjectSentrygun*>( info.GetInflictor() );
+ if ( pSentry )
+ {
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim, flRealDamage, dmg_from_sentry_reduced );
+ }
+ }
+
+ if ( IsMannVsMachineMode() )
+ {
+ if ( pTFAttacker && pTFAttacker->IsBot() && pAttacker != pVictimBaseEntity && pVictim && !pVictim->IsBot() )
+ {
+ flRealDamage *= g_pPopulationManager ? g_pPopulationManager->GetDamageMultiplier() : tf_populator_damage_multiplier.GetFloat();
+ }
+ }
+
+ // Heavy rage-based knockback+stun effect that also reduces their damage output
+ if ( pTFAttacker && pTFAttacker->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) )
+ {
+ int iRage = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pTFAttacker, iRage, generate_rage_on_dmg );
+ if ( iRage && pTFAttacker->m_Shared.IsRageDraining() )
+ {
+ flRealDamage *= 0.5f;
+ }
+ }
+
+ if ( pVictim && pTFAttacker && info.GetWeapon() )
+ {
+ CTFWeaponBase *pWeapon = pTFAttacker->GetActiveTFWeapon();
+ if ( pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_SNIPERRIFLE && info.GetWeapon() == pWeapon )
+ {
+ CTFSniperRifle *pRifle = static_cast< CTFSniperRifle* >( info.GetWeapon() );
+
+ float flStun = 1.0f;
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pRifle, flStun, applies_snare_effect );
+ if ( flStun != 1.0f )
+ {
+ float flDuration = pRifle->GetJarateTime();
+ pVictim->m_Shared.StunPlayer( flDuration, flStun, TF_STUN_MOVEMENT, pTFAttacker );
+ }
+ }
+ }
+
+ if ( pVictim && pVictim->GetActiveTFWeapon() )
+ {
+ if ( info.GetDamageType() & (DMG_CLUB) )
+ {
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim->GetActiveTFWeapon(), flRealDamage, dmg_from_melee );
+ }
+ else if ( info.GetDamageType() & (DMG_BLAST|DMG_BULLET|DMG_BUCKSHOT|DMG_IGNITE|DMG_SONIC) )
+ {
+ float flBeforeDamage = flRealDamage;
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pVictim->GetActiveTFWeapon(), flRealDamage, dmg_from_ranged );
+ PotentiallyFireDamageMitigatedEvent( pVictim, pVictim, pVictim->GetActiveTFWeapon(), flBeforeDamage, flRealDamage );
+ }
+ }
+
+ outParams.bSendPreFeignDamage = false;
+ if ( pVictim && pVictim->IsPlayerClass( TF_CLASS_SPY ) && ( info.GetDamageCustom() != TF_DMG_CUSTOM_TELEFRAG ) && !pVictim->IsTaunting() )
+ {
+ // STAGING_SPY
+ // Reduce damage taken if we have recently feigned death.
+ if ( pVictim->m_Shared.InCond( TF_COND_FEIGN_DEATH ) || pVictim->m_Shared.IsFeignDeathReady() )
+ {
+ // Damage reduction is proportional to cloak remaining (60%->20%)
+ float flDamageReduction = RemapValClamped( pVictim->m_Shared.GetSpyCloakMeter(), 50.0f, 0.0f, tf_feign_death_damage_scale.GetFloat(), tf_stealth_damage_reduction.GetFloat() );
+
+ // On Activate Reduce Remaining Cloak by 50%
+ if ( pVictim->m_Shared.IsFeignDeathReady() )
+ {
+ flDamageReduction = tf_feign_death_activate_damage_scale.GetFloat();
+ }
+ outParams.bSendPreFeignDamage = true;
+
+ float flBeforeflRealDamage = flRealDamage;
+
+ flRealDamage *= flDamageReduction;
+
+ CTFWeaponInvis *pWatch = (CTFWeaponInvis *) pVictim->Weapon_OwnsThisID( TF_WEAPON_INVIS );
+ PotentiallyFireDamageMitigatedEvent( pVictim, pVictim, pWatch, flBeforeflRealDamage, flRealDamage );
+
+ // Original damage would've killed the player, but the reduced damage wont
+ if ( flBeforeflRealDamage >= pVictim->GetHealth() && flRealDamage < pVictim->GetHealth() )
+ {
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "deadringer_cheat_death" );
+ if ( pEvent )
+ {
+ pEvent->SetInt( "spy", pVictim->GetUserID() );
+ pEvent->SetInt( "attacker", pTFAttacker ? pTFAttacker->GetUserID() : -1 );
+ gameeventmanager->FireEvent( pEvent, true );
+ }
+ }
+ }
+ // Standard Stealth gives small damage reduction
+ else if ( pVictim->m_Shared.InCond( TF_COND_STEALTHED ) )
+ {
+ flRealDamage *= tf_stealth_damage_reduction.GetFloat();
+ }
+ }
+
+ if ( flRealDamage == 0.0f )
+ {
+ // Do a hard out in the caller
+ return -1;
+ }
+
+ if ( pAttacker == pVictimBaseEntity && (info.GetDamageType() & DMG_BLAST) &&
+ info.GetDamagedOtherPlayers() == 0 && (info.GetDamageCustom() != TF_DMG_CUSTOM_TAUNTATK_GRENADE) )
+ {
+ // If we attacked ourselves, hurt no other players, and it is a blast,
+ // check the attribute that reduces rocket jump damage.
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( info.GetAttacker(), flRealDamage, rocket_jump_dmg_reduction );
+ outParams.bSelfBlastDmg = true;
+ }
+
+ if ( pAttacker == pVictimBaseEntity )
+ {
+ enum
+ {
+ kSelfBlastResponse_IgnoreProjectilesFromThisWeapon = 1, // the sticky jumper doesn't disable damage from other explosive weapons
+ kSelfBlastResponse_IgnoreProjectilesFromAllWeapons = 2, // the rocket jumper doesn't have a special projectile type and so ignores all self-inflicted damage from explosive sources
+ };
+
+ if ( info.GetWeapon() )
+ {
+ int iNoSelfBlastDamage = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iNoSelfBlastDamage, no_self_blast_dmg );
+
+ const bool bIgnoreThisSelfDamage = ( iNoSelfBlastDamage == kSelfBlastResponse_IgnoreProjectilesFromAllWeapons )
+ || ( (iNoSelfBlastDamage == kSelfBlastResponse_IgnoreProjectilesFromThisWeapon) && (info.GetDamageCustom() == TF_DMG_CUSTOM_PRACTICE_STICKY) );
+ if ( bIgnoreThisSelfDamage )
+ {
+ flRealDamage = 0;
+ }
+
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( info.GetWeapon(), flRealDamage, blast_dmg_to_self );
+ }
+ }
+
+ // Precision Powerup removes self damage
+ if ( pTFAttacker == pVictim && pTFAttacker->m_Shared.GetCarryingRuneType() == RUNE_PRECISION )
+ {
+ flRealDamage = 0.f;
+ }
+
+ if ( pTFAttacker && ( pTFAttacker != pVictim ) )
+ {
+ // Vampire Powerup collects health based on damage received on victim. Does not apply to self damage. Do it here to factor in victim resistance calculations
+ if ( pTFAttacker->m_Shared.GetCarryingRuneType() == RUNE_VAMPIRE )
+ {
+ if ( flRealDamage > 0 )
+ {
+ if ( pTFAttacker->GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_MINIGUN || pTFAttacker->GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_FLAMETHROWER )
+ {
+ pTFAttacker->TakeHealth( ( flRealDamage * 0.6f ), DMG_GENERIC );
+ }
+ else if ( info.GetDamageType() & DMG_MELEE )
+ {
+ pTFAttacker->TakeHealth( ( flRealDamage * 1.25f ), DMG_GENERIC );
+ }
+ else
+ {
+ pTFAttacker->TakeHealth( flRealDamage, DMG_GENERIC );
+ }
+ }
+ }
+
+ int iHypeOnDamage = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pTFAttacker, iHypeOnDamage, hype_on_damage );
+ if ( iHypeOnDamage )
+ {
+ float flHype = RemapValClamped( flRealDamage, 1.f, 200.f, 1.f, 50.f );
+ pTFAttacker->m_Shared.SetScoutHypeMeter( Min( 100.f, flHype + pTFAttacker->m_Shared.GetScoutHypeMeter() ) );
+ }
+ }
+ }
+
+ return flRealDamage;
+}
+
+// --------------------------------------------------------------------------------------------------- //
+// Voice helper
+// --------------------------------------------------------------------------------------------------- //
+
+class CVoiceGameMgrHelper : public IVoiceGameMgrHelper
+{
+public:
+ virtual bool CanPlayerHearPlayer( CBasePlayer *pListener, CBasePlayer *pTalker, bool &bProximity )
+ {
+ return TFGameRules()->TFVoiceManager( pListener, pTalker );
+ }
+};
+CVoiceGameMgrHelper g_VoiceGameMgrHelper;
+IVoiceGameMgrHelper *g_pVoiceGameMgrHelper = &g_VoiceGameMgrHelper;
+
+// Load the objects.txt file.
+class CObjectsFileLoad : public CAutoGameSystem
+{
+public:
+ virtual bool Init()
+ {
+ LoadObjectInfos( filesystem );
+ return true;
+ }
+} g_ObjectsFileLoad;
+
+// --------------------------------------------------------------------------------------------------- //
+// Globals.
+// --------------------------------------------------------------------------------------------------- //
+/*
+// NOTE: the indices here must match TEAM_UNASSIGNED, TEAM_SPECTATOR, TF_TEAM_RED, TF_TEAM_BLUE, etc.
+char *sTeamNames[] =
+{
+ "Unassigned",
+ "Spectator",
+ "Red",
+ "Blue"
+};
+*/
+// --------------------------------------------------------------------------------------------------- //
+// Global helper functions.
+// --------------------------------------------------------------------------------------------------- //
+
+// World.cpp calls this but we don't use it in TF.
+void InitBodyQue()
+{
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+CTFGameRules::~CTFGameRules()
+{
+ // Note, don't delete each team since they are in the gEntList and will
+ // automatically be deleted from there, instead.
+ TFTeamMgr()->Shutdown();
+ ShutdownCustomResponseRulesDicts();
+
+ // clean up cached teleport locations
+ m_mapTeleportLocations.PurgeAndDeleteElements();
+
+ // reset this only if we quit MvM to minimize the risk of breaking pub tournament
+ if ( IsMannVsMachineMode() )
+ {
+ mp_tournament.SetValue( 0 );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::CheckTauntAchievement( CTFPlayer *pAchiever, int nGibs, int *pTauntCamAchievements )
+{
+ if ( !pAchiever || !pAchiever->GetPlayerClass() )
+ return;
+
+ int iClass = pAchiever->GetPlayerClass()->GetClassIndex();
+ if ( pTauntCamAchievements[ iClass ] )
+ {
+ bool bAwardAchievement = true;
+
+ // for the Heavy achievement, the player needs to also be invuln
+ if ( iClass == TF_CLASS_HEAVYWEAPONS && pTauntCamAchievements[ iClass ] == ACHIEVEMENT_TF_HEAVY_FREEZECAM_TAUNT )
+ {
+ if ( !pAchiever->m_Shared.InCond( TF_COND_INVULNERABLE_WEARINGOFF ) && !pAchiever->m_Shared.InCond( TF_COND_INVULNERABLE ) )
+ {
+ bAwardAchievement = false;
+ }
+ }
+
+ // for the Spy achievement, we must be in the cig lighter taunt
+ if ( iClass == TF_CLASS_SPY && pTauntCamAchievements[ iClass ] == ACHIEVEMENT_TF_SPY_FREEZECAM_FLICK )
+ {
+ if ( pAchiever->GetActiveTFWeapon() && pAchiever->GetActiveTFWeapon()->GetWeaponID() != TF_WEAPON_PDA_SPY )
+ {
+ bAwardAchievement = false;
+ }
+ }
+
+ // for the two Sniper achievements, we need to check for specific taunts
+ if ( iClass == TF_CLASS_SNIPER )
+ {
+ if ( pTauntCamAchievements[ iClass ] == ACHIEVEMENT_TF_SNIPER_FREEZECAM_HAT )
+ {
+ if ( pAchiever->GetActiveTFWeapon() && pAchiever->GetActiveTFWeapon()->GetWeaponID() != TF_WEAPON_CLUB )
+ {
+ bAwardAchievement = false;
+ }
+ }
+ else if ( pTauntCamAchievements[ iClass ] == ACHIEVEMENT_TF_SNIPER_FREEZECAM_WAVE )
+ {
+ if ( pAchiever->GetActiveTFWeapon() && WeaponID_IsSniperRifle( pAchiever->GetActiveTFWeapon()->GetWeaponID() ) )
+ {
+ bAwardAchievement = false;
+ }
+ }
+ }
+
+ // For the Soldier achievements, we need to be doing a specific taunt, or have enough gibs onscreen
+ if ( iClass == TF_CLASS_SOLDIER )
+ {
+ if ( pTauntCamAchievements[ iClass ] == ACHIEVEMENT_TF_SOLDIER_FREEZECAM_TAUNT )
+ {
+ if ( pAchiever->GetActiveTFWeapon() && pAchiever->GetActiveTFWeapon()->GetWeaponID() != TF_WEAPON_SHOTGUN_SOLDIER )
+ {
+ bAwardAchievement = false;
+ }
+ }
+ else if ( pTauntCamAchievements[ iClass ] == ACHIEVEMENT_TF_SOLDIER_FREEZECAM_GIBS )
+ {
+ // Need at least 3 gibs on-screen
+ if ( nGibs < 3 )
+ {
+ bAwardAchievement = false;
+ }
+ }
+ }
+
+ // for the two Demoman achievements, we need to check for specific taunts
+ if ( iClass == TF_CLASS_DEMOMAN )
+ {
+ if ( pTauntCamAchievements[ iClass ] == ACHIEVEMENT_TF_DEMOMAN_FREEZECAM_SMILE )
+ {
+ if ( pAchiever->GetActiveTFWeapon() && pAchiever->GetActiveTFWeapon()->GetWeaponID() != TF_WEAPON_GRENADELAUNCHER )
+ {
+ bAwardAchievement = false;
+ }
+ }
+ else if ( pTauntCamAchievements[ iClass ] == ACHIEVEMENT_TF_DEMOMAN_FREEZECAM_RUMP )
+ {
+ if ( pAchiever->GetActiveTFWeapon() && pAchiever->GetActiveTFWeapon()->GetAttributeContainer() )
+ {
+ // Needs to be the Scottish Defender
+ CEconItemView *pItem = pAchiever->GetActiveTFWeapon()->GetAttributeContainer()->GetItem();
+ if ( pItem && pItem->IsValid() && pItem->GetItemDefIndex() != 130 ) // Scottish Defender is item index 130
+ {
+ bAwardAchievement = false;
+ }
+ }
+ }
+ }
+
+ // for the Engineer achievement, we must be in the guitar taunt
+ if ( iClass == TF_CLASS_ENGINEER && pTauntCamAchievements[ iClass ] == ACHIEVEMENT_TF_ENGINEER_FREEZECAM_TAUNT )
+ {
+ if ( pAchiever->GetActiveTFWeapon() && pAchiever->GetActiveTFWeapon()->GetWeaponID() != TF_WEAPON_SENTRY_REVENGE )
+ {
+ bAwardAchievement = false;
+ }
+ }
+
+ if ( bAwardAchievement )
+ {
+ pAchiever->AwardAchievement( pTauntCamAchievements[ iClass ] );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::TFVoiceManager( CBasePlayer *pListener, CBasePlayer *pTalker )
+{
+ // check coaching--we only want coaches and students to talk and listen to each other!
+ CTFPlayer* pTFListener = (CTFPlayer*)pListener;
+ CTFPlayer* pTFTalker = (CTFPlayer*)pTalker;
+ if ( pTFListener->GetStudent() || pTFListener->GetCoach() ||
+ pTFTalker->GetStudent() || pTFTalker->GetCoach() )
+ {
+ if ( pTFListener->GetStudent() == pTFTalker || pTFTalker->GetStudent() == pTFListener ||
+ pTFListener->GetCoach() == pTalker || pTFTalker->GetCoach() == pTFListener )
+ {
+ return true;
+ }
+ return false;
+ }
+
+ // Always allow teams to hear each other in TD mode
+ if ( IsMannVsMachineMode() )
+ return true;
+
+ if ( !tf_gravetalk.GetBool() )
+ {
+ // Dead players can only be heard by other dead team mates but only if a match is in progress
+ if ( State_Get() != GR_STATE_TEAM_WIN && State_Get() != GR_STATE_GAME_OVER )
+ {
+ if ( pTalker->IsAlive() == false )
+ {
+ if ( pListener->IsAlive() == false )
+ return ( pListener->InSameTeam( pTalker ) );
+
+ return false;
+ }
+ }
+ }
+
+ return ( pListener->InSameTeam( pTalker ) );
+
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: TF2 Specific Client Commands
+// Input :
+// Output :
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ClientCommand( CBaseEntity *pEdict, const CCommand &args )
+{
+ CTFPlayer *pPlayer = ToTFPlayer( pEdict );
+
+ const char *pcmd = args[0];
+
+ if ( IsInTournamentMode() == true && IsInPreMatch() == true )
+ {
+ if ( FStrEq( pcmd, "tournament_readystate" ) )
+ {
+ if ( IsMannVsMachineMode() )
+ return true;
+
+ if ( UsePlayerReadyStatusMode() )
+ return true;
+
+ if ( args.ArgC() < 2 )
+ return true;
+
+ if ( pPlayer->GetTeamNumber() <= LAST_SHARED_TEAM )
+ return true;
+
+ int iReadyState = atoi( args[1] );
+
+ //Already this state
+ if ( iReadyState == (int)IsTeamReady( pPlayer->GetTeamNumber() ) )
+ return true;
+
+ SetTeamReadyState( iReadyState == 1, pPlayer->GetTeamNumber() );
+
+ IGameEvent * event = gameeventmanager->CreateEvent( "tournament_stateupdate" );
+
+ if ( event )
+ {
+ event->SetInt( "userid", pPlayer->entindex() );
+ event->SetInt( "readystate", iReadyState );
+ event->SetBool( "namechange", 0 );
+ event->SetString( "oldname", " " );
+ event->SetString( "newname", " " );
+
+ gameeventmanager->FireEvent( event );
+ }
+
+ if ( iReadyState == 0 )
+ {
+ m_flRestartRoundTime.Set( -1.f );
+ m_bAwaitingReadyRestart = true;
+ }
+
+ return true;
+ }
+
+ if ( FStrEq( pcmd, "tournament_teamname" ) )
+ {
+ if ( IsMannVsMachineMode() )
+ return true;
+
+ if ( args.ArgC() < 2 )
+ return true;
+
+ if ( pPlayer->GetTeamNumber() <= LAST_SHARED_TEAM )
+ return true;
+
+ const char *commandline = args.GetCommandString();
+
+ // find the rest of the command line past the bot index
+ commandline = strstr( commandline, args[1] );
+ Assert( commandline );
+
+ char szName[MAX_TEAMNAME_STRING + 1] = { 0 };
+ Q_strncpy( szName, commandline, sizeof( szName ));
+
+
+ if ( pPlayer->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ if ( FStrEq( szName, mp_tournament_blueteamname.GetString() ) == true )
+ return true;
+
+ mp_tournament_blueteamname.SetValue( szName );
+ }
+ else if ( pPlayer->GetTeamNumber() == TF_TEAM_RED )
+ {
+ if ( FStrEq( szName, mp_tournament_redteamname.GetString() ) == true )
+ return true;
+
+ mp_tournament_redteamname.SetValue( szName );
+ }
+
+ IGameEvent * event = gameeventmanager->CreateEvent( "tournament_stateupdate" );
+
+ if ( event )
+ {
+ event->SetInt( "userid", pPlayer->entindex() );
+ event->SetBool( "readystate", 0 );
+ event->SetBool( "namechange", 1 );
+ event->SetString( "newname", szName );
+
+ gameeventmanager->FireEvent( event );
+ }
+
+ return true;
+ }
+
+ if ( FStrEq( pcmd, "tournament_player_readystate" ) )
+ {
+ if ( State_Get() != GR_STATE_BETWEEN_RNDS )
+ return true;
+
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc && pMatchDesc->m_params.m_bAutoReady )
+ return true;
+
+ // Make sure we have enough to allow ready mode commands
+ if ( !PlayerReadyStatus_HaveMinPlayersToEnable() )
+ return true;
+
+ if ( args.ArgC() < 2 )
+ return true;
+
+ bool bReady = ( atoi( args[1] ) == 1 );
+ PlayerReadyStatus_UpdatePlayerState( pPlayer, bReady );
+ if ( bReady )
+ {
+ pPlayer->PlayReadySound();
+ }
+
+ return true;
+ }
+ }
+
+ if ( FStrEq( pcmd, "objcmd" ) )
+ {
+ if ( args.ArgC() < 3 )
+ return true;
+
+ int entindex = atoi( args[1] );
+ edict_t* pEdict = INDEXENT(entindex);
+ if ( pEdict )
+ {
+ CBaseEntity* pBaseEntity = GetContainingEntity(pEdict);
+ CBaseObject* pObject = dynamic_cast<CBaseObject*>(pBaseEntity);
+
+ if ( pObject )
+ {
+ // We have to be relatively close to the object too...
+
+ // BUG! Some commands need to get sent without the player being near the object,
+ // eg delayed dismantle commands. Come up with a better way to ensure players aren't
+ // entering these commands in the console.
+
+ //float flDistSq = pObject->GetAbsOrigin().DistToSqr( pPlayer->GetAbsOrigin() );
+ //if (flDistSq <= (MAX_OBJECT_SCREEN_INPUT_DISTANCE * MAX_OBJECT_SCREEN_INPUT_DISTANCE))
+ {
+ // Strip off the 1st two arguments and make a new argument string
+ CCommand objectArgs( args.ArgC() - 2, &args.ArgV()[2]);
+ pObject->ClientCommand( pPlayer, objectArgs );
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // Handle some player commands here as they relate more directly to gamerules state
+ if ( FStrEq( pcmd, "nextmap" ) )
+ {
+ if ( pPlayer->m_flNextTimeCheck < gpGlobals->curtime )
+ {
+ char szNextMap[MAX_MAP_NAME];
+
+ if ( nextlevel.GetString() && *nextlevel.GetString() )
+ {
+ Q_strncpy( szNextMap, nextlevel.GetString(), sizeof( szNextMap ) );
+ }
+ else
+ {
+ GetNextLevelName( szNextMap, sizeof( szNextMap ) );
+ }
+
+ ClientPrint( pPlayer, HUD_PRINTTALK, "#TF_nextmap", szNextMap);
+
+ pPlayer->m_flNextTimeCheck = gpGlobals->curtime + 1;
+ }
+
+ return true;
+ }
+ else if ( FStrEq( pcmd, "timeleft" ) )
+ {
+ if ( pPlayer->m_flNextTimeCheck < gpGlobals->curtime )
+ {
+ if ( mp_timelimit.GetInt() > 0 )
+ {
+ int iTimeLeft = GetTimeLeft();
+
+ char szMinutes[5];
+ char szSeconds[3];
+
+ if ( iTimeLeft <= 0 )
+ {
+ Q_snprintf( szMinutes, sizeof(szMinutes), "0" );
+ Q_snprintf( szSeconds, sizeof(szSeconds), "00" );
+ }
+ else
+ {
+ Q_snprintf( szMinutes, sizeof(szMinutes), "%d", iTimeLeft / 60 );
+ Q_snprintf( szSeconds, sizeof(szSeconds), "%02d", iTimeLeft % 60 );
+ }
+
+ ClientPrint( pPlayer, HUD_PRINTTALK, "#TF_timeleft", szMinutes, szSeconds );
+ }
+ else
+ {
+ ClientPrint( pPlayer, HUD_PRINTTALK, "#TF_timeleft_nolimit" );
+ }
+
+ pPlayer->m_flNextTimeCheck = gpGlobals->curtime + 1;
+ }
+ return true;
+ }
+#ifdef STAGING_ONLY
+ else if ( FStrEq( pcmd, "mvm_allupgrades" ) )
+ {
+ if ( GameModeUsesUpgrades() && g_hUpgradeEntity )
+ {
+ g_hUpgradeEntity->GrantOrRemoveAllUpgrades( pPlayer );
+ }
+
+ return true;
+ }
+#endif
+ else if( pPlayer->ClientCommand( args ) )
+ {
+ return true;
+ }
+
+ return BaseClass::ClientCommand( pEdict, args );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::LevelShutdown()
+{
+ if ( IsInTraining() )
+ {
+ mp_humans_must_join_team.SetValue( "any" );
+ training_can_build_sentry.Revert();
+ training_can_build_dispenser.Revert();
+ training_can_build_tele_entrance.Revert();
+ training_can_build_tele_exit.Revert();
+ training_can_destroy_buildings.Revert();
+ training_can_pickup_sentry.Revert();
+ training_can_pickup_dispenser.Revert();
+ training_can_pickup_tele_entrance.Revert();
+ training_can_pickup_tele_exit.Revert();
+ training_can_select_weapon_primary.Revert();
+ training_can_select_weapon_secondary.Revert();
+ training_can_select_weapon_melee.Revert();
+ training_can_select_weapon_building.Revert();
+ training_can_select_weapon_pda.Revert();
+ training_can_select_weapon_item1.Revert();
+ training_can_select_weapon_item2.Revert();
+ tf_training_client_message.Revert();
+ }
+ TheTFBots().LevelShutdown();
+ hide_server.Revert();
+
+ DuelMiniGame_LevelShutdown();
+ GameCoordinator_NotifyLevelShutdown();
+
+ g_TFGameModeHistory.SetPrevState( m_nGameType );
+
+ if ( m_pUpgrades )
+ {
+ UTIL_Remove( m_pUpgrades );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::Think()
+{
+
+ if ( m_bMapCycleNeedsUpdate )
+ {
+ m_bMapCycleNeedsUpdate = false;
+ LoadMapCycleFile();
+ }
+
+ if ( g_fGameOver )
+ {
+ if ( UsePlayerReadyStatusMode() && !IsMannVsMachineMode() )
+ {
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+
+ static int nLastTimeSent = -1;
+ int nTimeLeft = ( m_flStateTransitionTime - gpGlobals->curtime );
+ int nTimePassed = gpGlobals->curtime - m_flLastRoundStateChangeTime;
+ if ( pMatchDesc && pMatchDesc->m_params.m_pszMatchEndKickWarning && nTimeLeft <= 50 && nTimeLeft % 10 == 0 && nTimeLeft != nLastTimeSent )
+ {
+ nLastTimeSent = nTimeLeft;
+ CBroadcastRecipientFilter filter;
+ UTIL_ClientPrintFilter( filter, HUD_PRINTTALK, pMatchDesc->m_params.m_pszMatchEndKickWarning, CFmtStr( "%d", nTimeLeft ) );
+ }
+
+ if ( BAttemptMapVoteRollingMatch() )
+ {
+ const CMatchInfo* pMatch = GTFGCClientSystem()->GetMatch();
+ if ( pMatch && pMatch->GetNumActiveMatchPlayers() == 0 )
+ {
+ Msg( "All players left during next map voting period. Ending match.\n" );
+ GTFGCClientSystem()->EndManagedMatch( /* bKickPlayersToParties */ false );
+ Assert( IsManagedMatchEnded() );
+ m_bMatchEnded.Set( true );
+ return;
+ }
+
+ if ( m_eRematchState == NEXT_MAP_VOTE_STATE_WAITING_FOR_USERS_TO_VOTE )
+ {
+ bool bVotePeriodExpired = false;
+ // Judgment time has arrived. Force a result below
+ if ( nTimePassed == tf_mm_next_map_vote_time.GetInt() )
+ {
+ bVotePeriodExpired = true;
+ }
+
+ int nVotes[ EUserNextMapVote::NUM_VOTE_STATES ];
+ EUserNextMapVote eWinningVote = GetWinningVote( nVotes );
+
+ if ( bVotePeriodExpired ||
+ ( nVotes[ USER_NEXT_MAP_VOTE_UNDECIDED ] == 0 && eWinningVote != USER_NEXT_MAP_VOTE_UNDECIDED ) )
+ {
+ CBroadcastRecipientFilter filter;
+
+ const MapDef_t *pMap = NULL;
+ if ( eWinningVote == USER_NEXT_MAP_VOTE_UNDECIDED )
+ {
+ // Nobody voted! We're playing on the same map again by default
+ pMap = GetItemSchema()->GetMasterMapDefByName( STRING( gpGlobals->mapname ) );
+ }
+ else
+ {
+ pMap = GetItemSchema()->GetMasterMapDefByIndex( GetNextMapVoteOption( eWinningVote ) );
+ }
+
+ if ( pMap == NULL )
+ {
+ Assert( !"We somehow didn't pick a new map to rotate to! Default to the current one" );
+ pMap = GetItemSchema()->GetMasterMapDefByName( STRING( gpGlobals->mapname ) );
+ }
+
+ if ( pMap )
+ {
+ m_eRematchState = NEXT_MAP_VOTE_STATE_MAP_CHOSEN_PAUSE;
+ GTFGCClientSystem()->RequestNewMatchForLobby( pMap );
+ }
+ }
+ }
+ else if ( m_eRematchState == NEXT_MAP_VOTE_STATE_MAP_CHOSEN_PAUSE )
+ {
+ // CTFGCServerSystem is in control at this point
+ }
+ }
+
+ if ( gpGlobals->curtime > m_flStateTransitionTime || !BHavePlayers() )
+ {
+ nLastTimeSent = -1;
+ if ( pMatchDesc )
+ {
+ // Matchmaking path
+ pMatchDesc->PostMatchClearServerSettings();
+ }
+ else
+ {
+ // Readymode (Tournament) path
+ g_fGameOver = false;
+ m_bAllowBetweenRounds = true;
+ State_Transition( GR_STATE_RESTART );
+ SetInWaitingForPlayers( true );
+ }
+ return;
+ }
+ }
+
+ if ( ( m_flMatchSummaryTeleportTime > 0 ) && ( gpGlobals->curtime > m_flMatchSummaryTeleportTime ) )
+ {
+ m_flMatchSummaryTeleportTime = -1.f;
+ MatchSummaryTeleport();
+ }
+ }
+ else
+ {
+ if ( gpGlobals->curtime > m_flNextPeriodicThink )
+ {
+ if ( State_Get() != GR_STATE_BONUS && State_Get() != GR_STATE_TEAM_WIN && State_Get() != GR_STATE_GAME_OVER && IsInWaitingForPlayers() == false )
+ {
+ if ( CheckCapsPerRound() )
+ return;
+ }
+ }
+
+ // These network variables mirror the MM system's match state for client's sake. Gamerules should still
+ // be aware of when these change, either because we caused it or via a callback. This warning will
+ // detect desync. (Ideally we'd have the ability to just share between the client GC system and server
+ // GC system directly without passing things through gamerules)
+ if ( m_bMatchEnded != IsManagedMatchEnded() )
+ {
+ Assert( false );
+ Warning( "Mirrored Match parameters on gamerules don't match MatchInfo\n" );
+ m_bMatchEnded.Set( IsManagedMatchEnded() );
+ }
+
+ if ( GTFGCClientSystem()->GetMatch() && GetCurrentMatchGroup() != (EMatchGroup)m_nMatchGroupType.Get() )
+ {
+ Assert( false );
+ Warning( "Mirrored Match parameters on gamerules don't match MatchInfo\n" );
+ m_nMatchGroupType.Set( GetCurrentMatchGroup() );
+ }
+
+ // Managed matches (MvM and competitive) abandon thing
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ bool bEveryoneSafeToLeave = true;
+ if ( pMatch )
+ {
+ // Send current safe-to-leave flags down from the GCServerSystem
+ for ( int i = 1; i <= gpGlobals->maxClients; ++i )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( !pPlayer )
+ { continue; }
+
+ CSteamID steamID;
+ bool bSafe = !pMatch || !pPlayer->GetSteamID( &steamID ) || pMatch->BPlayerSafeToLeaveMatch( steamID );
+
+ pPlayer->SetMatchSafeToLeave( bSafe );
+ bEveryoneSafeToLeave = bEveryoneSafeToLeave && bSafe;
+ }
+ }
+
+ if ( IsCompetitiveMode() )
+ {
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ Assert( pMatch ); // Should not be in competitive mode without a match
+
+ //
+ // Check if this is mode requires a complete match, but doesn't have one
+ //
+ bool bEndMatch = false;
+ int nActiveMatchPlayers = pMatch->GetNumActiveMatchPlayers();
+ int nMissingPlayers = pMatch->GetCanonicalMatchSize() - nActiveMatchPlayers;
+ if ( pMatchDesc->m_params.m_bRequireCompleteMatch &&
+ !IsManagedMatchEnded() &&
+ nMissingPlayers )
+ {
+ // See if we are requesting late join right now, and give that time to work
+ if ( pMatchDesc->ShouldRequestLateJoin() )
+ {
+ // End match if GC system didn't request late join in response to players leaving
+ auto *pGCSys = GTFGCClientSystem();
+ double flRequestedLateJoin = pGCSys->GetTimeRequestedLateJoin();
+
+ if ( flRequestedLateJoin == -1.f )
+ {
+ bEndMatch = true;
+ Msg( "Failed to request late join, ending competitive match\n" );
+ }
+ else
+ {
+ // Otherwise, since we can't proceed without players, apply a timeout after which we'll
+ // cancel the match and release these players. The time to wait is shorter if the GC
+ // hasn't confirmed our late join request, so we're not spending the full time waiting
+ // when the GC is just non-responsive.
+ double flTimeWaitingForLateJoin = CRTime::RTime32TimeCur() - flRequestedLateJoin;
+ bool bGotLateJoin = pGCSys->BLateJoinEligible();
+ double flWaitLimit = bGotLateJoin ? tf_competitive_required_late_join_timeout.GetFloat()
+ : tf_competitive_required_late_join_confirm_timeout.GetFloat();
+ if ( flTimeWaitingForLateJoin > flWaitLimit )
+ {
+ Msg( "Exceeded wait time limit for late joiners, canceling match\n" );
+ bEndMatch = true;
+ }
+ }
+ }
+ else
+ {
+ // Can't request late joiners, tank match if number of active players get below some threshold
+ int iRedActive = 0;
+ int iBlueActive = 0;
+ for ( int idxPlayer = 0; idxPlayer < pMatch->GetNumTotalMatchPlayers(); idxPlayer++ )
+ {
+ CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( idxPlayer );
+ if ( !pMatchPlayer->bDropped )
+ {
+ int iTeam = GetGameTeamForGCTeam( pMatchPlayer->eGCTeam );
+ if ( iTeam == TF_TEAM_RED )
+ iRedActive++;
+ else
+ iBlueActive++;
+ }
+ }
+
+ int iTeamSize = pMatch->GetCanonicalMatchSize() / 2;
+ if ( iRedActive == 0 || iBlueActive == 0 || ( iTeamSize - iRedActive ) > tf_mm_abandoned_players_per_team_max.GetInt() || ( iTeamSize - iBlueActive ) > tf_mm_abandoned_players_per_team_max.GetInt() )
+ {
+ Msg( "Match type requires a complete match, but there are not enough active players left and we are not requesting late join. Stopping match.\n" );
+ bEndMatch = true;
+ }
+ }
+ }
+ else if ( !IsManagedMatchEnded() && nActiveMatchPlayers < 1 )
+ {
+ // For non-complete mode, just stop the match if we lose all players
+ Msg( "Competitive managed match in progress, but no remaining match players. Stopping match.\n" );
+ bEndMatch = true;
+ }
+
+ if ( bEndMatch )
+ {
+ StopCompetitiveMatch( CMsgGC_Match_Result_Status_MATCH_FAILED_ABANDON );
+ }
+ else
+ {
+ // If the match was ended but we're still playing, kick off a timer to remind people that
+ // they're in a dead match.
+ AssertMsg( !IsManagedMatchEnded() || ( pMatch->BMatchTerminated() && bEveryoneSafeToLeave ),
+ "Expect everyone to be safe to leave and the match info to reflect that after the match is over" );
+ bool bGameRunning = ( State_Get() == GR_STATE_BETWEEN_RNDS || State_Get() == GR_STATE_RND_RUNNING );
+ bool bDeadMatch = bGameRunning && IsManagedMatchEnded() && pMatch->BMatchTerminated() && bEveryoneSafeToLeave;
+ if ( bDeadMatch && ( m_flSafeToLeaveTimer == -.1f ||
+ m_flSafeToLeaveTimer - gpGlobals->curtime <= 0.f ) )
+ {
+ // Periodic nag event
+ m_flSafeToLeaveTimer = gpGlobals->curtime + 30.f;
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "player_abandoned_match" );
+ if ( pEvent )
+ {
+ pEvent->SetBool( "game_over", false );
+ gameeventmanager->FireEvent( pEvent );
+ }
+ }
+ }
+
+ // Handle re-spawning the players after the doors have shut at the beginning of a match
+ if ( ( m_flCompModeRespawnPlayersAtMatchStart > 0 ) && ( m_flCompModeRespawnPlayersAtMatchStart < gpGlobals->curtime ) )
+ {
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CTFPlayer *pPlayer = static_cast<CTFPlayer*>( UTIL_PlayerByIndex( i ) );
+ if ( !pPlayer )
+ continue;
+
+ pPlayer->RemoveAllOwnedEntitiesFromWorld();
+ pPlayer->ForceRespawn();
+ }
+
+ m_flCompModeRespawnPlayersAtMatchStart = -1.f;
+ }
+ }
+ }
+
+ if ( g_bRandomMap == true )
+ {
+ g_bRandomMap = false;
+
+ char szNextMap[MAX_MAP_NAME];
+ GetNextLevelName( szNextMap, sizeof(szNextMap), true );
+ IncrementMapCycleIndex();
+
+ ChangeLevelToMap( szNextMap );
+
+ return;
+ }
+
+ if ( IsInArenaMode() == true )
+ {
+ if ( m_flSendNotificationTime > 0.0f && m_flSendNotificationTime <= gpGlobals->curtime )
+ {
+ Arena_SendPlayerNotifications();
+ }
+ }
+
+ // periodically count up the fake clients and set the bot_count cvar to update server tags
+ if ( m_botCountTimer.IsElapsed() )
+ {
+ m_botCountTimer.Start( 5.0f );
+
+ int botCount = 0;
+ for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ )
+ {
+ CTFPlayer *player = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( player && player->IsFakeClient() )
+ {
+ ++botCount;
+ }
+ }
+
+ tf_bot_count.SetValue( botCount );
+ }
+
+#ifdef GAME_DLL
+#ifdef _DEBUG
+ if ( tf_debug_ammo_and_health.GetBool() )
+ {
+ CBaseEntity *ent;
+
+ for( int i=0; i<m_healthVector.Count(); ++i )
+ {
+ ent = m_healthVector[i];
+ if ( ent )
+ {
+ NDebugOverlay::Cross3D( ent->WorldSpaceCenter(), 10.0f, 0, 255, 0, true, 0.1f );
+ }
+ }
+
+ for( int i=0; i<m_ammoVector.Count(); ++i )
+ {
+ ent = m_ammoVector[i];
+ if ( ent )
+ {
+ NDebugOverlay::Cross3D( ent->WorldSpaceCenter(), 10.0f, 0, 0, 255, true, 0.1f );
+ }
+ }
+ }
+#endif // _DEBUG
+
+ if ( g_voteController )
+ {
+ ManageServerSideVoteCreation();
+ }
+
+ // ...
+ if ( tf_item_based_forced_holiday.GetInt() == kHoliday_Halloween && engine->Time() >= g_fEternaweenAutodisableTime )
+ {
+ if ( GCClientSystem() )
+ {
+ GCSDK::CProtoBufMsg<CMsgGC_GameServer_ServerModificationItemExpired> msg( k_EMsgGC_GameServer_ServerModificationItemExpired );
+ msg.Body().set_modification_type( kGameServerModificationItem_Halloween );
+ GCClientSystem()->BSendMessage( msg );
+ }
+
+ tf_item_based_forced_holiday.SetValue( kHoliday_None );
+ FlushAllAttributeCaches();
+ }
+
+ // play the bomb alarm if we need to
+ if ( m_bMannVsMachineAlarmStatus )
+ {
+ if ( m_flNextFlagAlert < gpGlobals->curtime )
+ {
+ if ( PlayThrottledAlert( 255, "Announcer.MVM_Bomb_Alert_Near_Hatch", 5.0f ) )
+ {
+ m_flNextFlagAlarm = gpGlobals->curtime + 3.0;
+ m_flNextFlagAlert = gpGlobals->curtime + 20.0f;
+ }
+ }
+
+ if ( m_flNextFlagAlarm < gpGlobals->curtime )
+ {
+ m_flNextFlagAlarm = gpGlobals->curtime + 3.0;
+
+ BroadcastSound( 255, "MVM.BombWarning" );
+ }
+ }
+ else if ( m_flNextFlagAlarm > 0.0f )
+ {
+ m_flNextFlagAlarm = 0.0f;
+ m_flNextFlagAlert = gpGlobals->curtime + 5.0f;
+ }
+
+ if ( m_bPowerupImbalanceMeasuresRunning )
+ {
+ if ( m_flTimeToStopImbalanceMeasures < gpGlobals->curtime )
+ {
+ PowerupTeamImbalance( TEAM_UNASSIGNED ); // passing TEAM_UNASSIGNED will fire the ImbalanceMeasuresOver output
+ m_bPowerupImbalanceMeasuresRunning = false;
+ }
+ }
+
+ PeriodicHalloweenUpdate();
+ SpawnHalloweenBoss();
+
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ if ( ( State_Get() == GR_STATE_RND_RUNNING ) && ( !m_bHelltowerPlayersInHell ) && ( m_helltowerTimer.IsElapsed() ) )
+ {
+ // Play our Halloween winning/losing lines for the teams
+ int iWinningTeam = TEAM_UNASSIGNED;
+ bool bRareLine = ( RandomFloat( 0, 1 ) < HELLTOWER_RARE_LINE_CHANCE );
+ float flRedProgress = 0.0f, flBlueProgress = 0.0f;
+ for ( int i = 0 ; i < ITFTeamTrainWatcher::AutoList().Count() ; ++i )
+ {
+ CTeamTrainWatcher *pTrainWatcher = static_cast< CTeamTrainWatcher* >( ITFTeamTrainWatcher::AutoList()[i] );
+ if ( !pTrainWatcher->IsDisabled() )
+ {
+ if ( pTrainWatcher->GetTeamNumber() == TF_TEAM_RED )
+ {
+ flRedProgress = pTrainWatcher->GetTrainDistanceAlongTrack();
+ }
+ else
+ {
+ flBlueProgress = pTrainWatcher->GetTrainDistanceAlongTrack();
+ }
+ }
+ }
+
+ if ( flRedProgress > flBlueProgress )
+ {
+ iWinningTeam = TF_TEAM_RED;
+ }
+ else if ( flBlueProgress > flRedProgress )
+ {
+ iWinningTeam = TF_TEAM_BLUE;
+ }
+
+ int iRedLine = HELLTOWER_VO_RED_MISC;
+ int iBlueLine = HELLTOWER_VO_BLUE_MISC;
+
+ // should we play the misc lines or the winning/losing lines?
+ if ( ( iWinningTeam == TEAM_UNASSIGNED ) || ( RandomFloat( 0, 1 ) < HELLTOWER_MISC_CHANCE ) )
+ {
+ if ( bRareLine )
+ {
+ iRedLine = HELLTOWER_VO_RED_MISC_RARE;
+ iBlueLine = HELLTOWER_VO_BLUE_MISC_RARE;
+ }
+ }
+ else
+ {
+ // play a winning/losing line
+ iRedLine = ( iWinningTeam == TF_TEAM_RED ) ? HELLTOWER_VO_RED_WINNING : HELLTOWER_VO_RED_LOSING;
+ iBlueLine = ( iWinningTeam == TF_TEAM_BLUE ) ? HELLTOWER_VO_BLUE_WINNING : HELLTOWER_VO_BLUE_LOSING;
+
+ if ( bRareLine )
+ {
+ iRedLine = ( iWinningTeam == TF_TEAM_RED ) ? HELLTOWER_VO_RED_WINNING_RARE : HELLTOWER_VO_RED_LOSING_RARE;
+ iBlueLine = ( iWinningTeam == TF_TEAM_BLUE ) ? HELLTOWER_VO_BLUE_WINNING_RARE : HELLTOWER_VO_BLUE_LOSING_RARE;
+
+ }
+ }
+
+ PlayHelltowerAnnouncerVO( iRedLine, iBlueLine );
+ m_helltowerTimer.Start( HELLTOWER_TIMER_INTERVAL );
+ }
+ }
+ else if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_DOOMSDAY ) )
+ {
+ if ( ( State_Get() == GR_STATE_RND_RUNNING ) && m_doomsdaySetupTimer.HasStarted() && m_doomsdaySetupTimer.IsElapsed() )
+ {
+ m_doomsdaySetupTimer.Invalidate();
+
+ const char *pszSound = NULL;
+ switch( GetRoundsPlayed() )
+ {
+ case 0:
+ pszSound = "sf14.Merasmus.Start.FirstRound";
+ if ( RandomInt( 1, 10 ) == 1 )
+ {
+ pszSound = "sf14.Merasmus.Start.FirstRoundRare";
+ }
+ break;
+ case 1:
+ pszSound = "sf14.Merasmus.Start.SecondRound";
+ break;
+ case 2:
+ default:
+ pszSound = "sf14.Merasmus.Start.ThirdRoundAndBeyond";
+ break;
+ }
+
+ if ( pszSound && pszSound[0] )
+ {
+ BroadcastSound( 255, pszSound );
+ }
+ }
+ }
+
+#ifdef TF_RAID_MODE
+ // This check is here for Boss battles that don't have a tf_raid_logic entity
+ if ( IsBossBattleMode() && !IsInWaitingForPlayers() && State_Get() == GR_STATE_RND_RUNNING )
+ {
+ CUtlVector< CTFPlayer * > alivePlayerVector;
+ CollectPlayers( &alivePlayerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS );
+
+ // if everyone is dead at the same time, they lose
+ if ( alivePlayerVector.Count() == 0 )
+ {
+ SetWinningTeam( TF_TEAM_RED, WINREASON_OPPONENTS_DEAD );
+ }
+ }
+#endif // TF_RAID_MODE
+
+ // Batched strange event message processing?
+ if ( engine->Time() > m_flNextStrangeEventProcessTime )
+ {
+ KillEaterEvents_FlushBatches();
+ m_flNextStrangeEventProcessTime = engine->Time() + g_flStrangeEventBatchProcessInterval;
+ }
+
+ ManageCompetitiveMode();
+
+#endif // GAME_DLL
+
+//=============================================================================
+// HPE_BEGIN:
+// [msmith] Do training summary screen logic.
+//=============================================================================
+ if ( IsInTraining() == true )
+ {
+ if ( m_flStateTransitionTime > gpGlobals->curtime )
+ {
+ int client_message = tf_training_client_message.GetInt();
+ switch ( client_message )
+ {
+ case TRAINING_CLIENT_MESSAGE_IN_SUMMARY_SCREEN:
+ {
+ //Keep adding time to the restart while we are in the end screen menu so that we never restart the round until
+ //the player presses the replay button (or next button to go to the next map).
+ m_flStateTransitionTime = gpGlobals->curtime + 5.0f;
+ }
+ break;
+ case TRAINING_CLIENT_MESSAGE_NEXT_MAP:
+ {
+ LoadNextTrainingMap();
+ tf_training_client_message.SetValue( (int)TRAINING_CLIENT_MESSAGE_NONE );
+ }
+ break;
+ case TRAINING_CLIENT_MESSAGE_REPLAY:
+ {
+ // Reload the map
+ engine->ChangeLevel( STRING( gpGlobals->mapname ), NULL );
+ tf_training_client_message.SetValue( (int)TRAINING_CLIENT_MESSAGE_NONE );
+ }
+ break;
+ } // switch
+ }
+ }
+//=============================================================================
+// HPE_END
+//=============================================================================
+
+ // This is ugly, but, population manager needs to sometimes think when we're not simulating, but it is an entity.
+ // Really, we need to better split out some kind of "sub-gamerules" class for modes like this.
+ if ( IsMannVsMachineMode() && g_pPopulationManager )
+ {
+ g_pPopulationManager->GameRulesThink();
+ }
+
+ BaseClass::Think();
+}
+
+#ifdef GAME_DLL
+#ifdef STAGING_ONLY
+ConVar tf_spawn_halloween_gift_test_enabled( "tf_spawn_halloween_gift_test_enabled", "0", 0, "enable to spawn a gift at the world origin. You probably want to use ConCommand 'SpawnHalloweenGiftTest'" );
+
+CON_COMMAND_F( tf_spawn_halloween_gift_test, "Test Halloween Gifts", FCVAR_NONE )
+{
+ ConVarRef gift_test( "tf_spawn_halloween_gift_test_enabled" );
+ gift_test.SetValue( 1 );
+}
+#endif // STAGING_ONLY
+
+void CTFGameRules::PeriodicHalloweenUpdate()
+{
+ // DEBUG
+#ifdef STAGING_ONLY
+ if ( tf_spawn_halloween_gift_test_enabled.GetBool() )
+ {
+ m_flNextHalloweenGiftUpdateTime = gpGlobals->curtime;
+ tf_spawn_halloween_gift_test_enabled.SetValue( 0 );
+ return;
+ }
+#endif //staging_only
+
+ // Are we on a Halloween Map?
+ // Do we have Halloween Contracts?
+ if ( !IsHolidayActive( kHoliday_Halloween ) )
+ return;
+
+ // Loop through each player that has a quest and spawn them a gift
+ if ( m_halloweenGiftSpawnLocations.Count() == 0 )
+ return;
+
+ // If we've never given out gifts before, set the time
+ if ( m_flNextHalloweenGiftUpdateTime < 0 )
+ {
+ m_flNextHalloweenGiftUpdateTime = gpGlobals->curtime + RandomInt( 7, 12 ) * 60;
+ return;
+ }
+
+ if ( m_flNextHalloweenGiftUpdateTime > gpGlobals->curtime )
+ return;
+
+ m_flNextHalloweenGiftUpdateTime = gpGlobals->curtime + RandomInt( 7, 12 ) * 60;
+
+ CUtlVector< CTFPlayer* > playerVector;
+ CollectPlayers( &playerVector );
+ FOR_EACH_VEC( playerVector, i )
+ {
+ Vector vLocation = m_halloweenGiftSpawnLocations.Element( RandomInt( 0, m_halloweenGiftSpawnLocations.Count() - 1 ) );
+ CHalloweenGiftPickup *pGift = assert_cast<CHalloweenGiftPickup*>( CBaseEntity::CreateNoSpawn( "tf_halloween_gift_pickup", vLocation, vec3_angle, NULL ) );
+ if ( pGift )
+ {
+ pGift->SetTargetPlayer( playerVector[i] );
+ DispatchSpawn( pGift );
+ }
+ }
+
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::SwitchToNextBestWeapon( CBaseCombatCharacter *pPlayer, CBaseCombatWeapon *pCurrentWeapon )
+{
+ if ( pPlayer )
+ {
+ CBaseCombatWeapon *lastWeapon = ToTFPlayer( pPlayer )->GetLastWeapon();
+
+ if ( lastWeapon != NULL && lastWeapon->HasAnyAmmo() )
+ {
+ return pPlayer->Weapon_Switch( lastWeapon );
+ }
+ }
+
+ return BaseClass::SwitchToNextBestWeapon( pPlayer, pCurrentWeapon );
+}
+
+
+#ifdef GAME_DLL
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::PointsMayBeCaptured( void )
+{
+ if ( IsHolidayActive( kHoliday_Halloween ) && GetActiveBoss() )
+ {
+ switch ( GetHalloweenScenario() )
+ {
+ case HALLOWEEN_SCENARIO_VIADUCT:
+ {
+ // the eyeball prevents point capturing while he's in play
+ if ( assert_cast< CEyeballBoss * >( GetActiveBoss() ) )
+ {
+ return false;
+ }
+ }
+ break;
+ case HALLOWEEN_SCENARIO_LAKESIDE:
+ {
+ // merasmus prevents point capturing while he's in play
+ if ( assert_cast< CMerasmus * >( GetActiveBoss() ) )
+ {
+ return false;
+ }
+ }
+ break;
+ }
+ }
+
+ if ( IsMannVsMachineMode() )
+ {
+ return true;
+ }
+
+ return BaseClass::PointsMayBeCaptured();
+}
+
+
+extern bool IsSpaceToSpawnHere( const Vector &where );
+
+static bool isZombieMobForceSpawning = false;
+
+#ifdef STAGING_ONLY
+// force the boss to spawn where our cursor is pointing
+CON_COMMAND_F( tf_halloween_force_zombie_mob, "For testing.", FCVAR_CHEAT )
+{
+ isZombieMobForceSpawning = true;
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SpawnZombieMob( void )
+{
+ if ( !tf_halloween_zombie_mob_enabled.GetBool() )
+ {
+ return;
+ }
+
+ // timer was started and has elapsed - time to spawn the boss
+ if ( InSetup() || IsInWaitingForPlayers() )
+ {
+ m_zombieMobTimer.Start( tf_halloween_zombie_mob_spawn_interval.GetFloat() );
+ return;
+ }
+
+ if ( isZombieMobForceSpawning )
+ {
+ isZombieMobForceSpawning = false;
+ m_zombieMobTimer.Invalidate();
+ }
+
+ // spawn pending mob members
+ if ( m_zombiesLeftToSpawn > 0 )
+ {
+ if ( IsSpaceToSpawnHere( m_zombieSpawnSpot ) )
+ {
+ if ( CZombie::SpawnAtPos( m_zombieSpawnSpot ) )
+ {
+ --m_zombiesLeftToSpawn;
+ }
+ }
+ }
+
+ // require a minimum number of human players in the game before the boss appears
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS );
+ CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS );
+
+ // only count humans
+ int totalPlayers = 0;
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ if ( !playerVector[i]->IsBot() )
+ {
+ ++totalPlayers;
+ }
+ }
+
+ if ( totalPlayers == 0 )
+ {
+ return;
+ }
+
+ // spawn a mob
+ if ( m_zombieMobTimer.IsElapsed() )
+ {
+ m_zombieMobTimer.Start( tf_halloween_zombie_mob_spawn_interval.GetFloat() );
+
+ CUtlVector< CTFNavArea * > ambushVector; // vector of hidden but near-to-victim areas
+
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ CTFPlayer *player = playerVector[i];
+
+ if ( player->IsBot() )
+ {
+ continue;
+ }
+
+ if ( !player->GetLastKnownArea() )
+ {
+ continue;
+ }
+
+ const float maxSurroundTravelRange = 2000.0f;
+
+ CUtlVector< CNavArea * > areaVector;
+
+ // collect walkable areas surrounding this player
+ CollectSurroundingAreas( &areaVector, player->GetLastKnownArea(), maxSurroundTravelRange, StepHeight, StepHeight );
+
+ // keep subset that isn't visible to any player
+ for( int j=0; j<areaVector.Count(); ++j )
+ {
+ CTFNavArea *area = (CTFNavArea *)areaVector[j];
+
+ if ( !area->IsValidForWanderingPopulation() )
+ {
+ continue;
+ }
+
+ if ( area->IsPotentiallyVisibleToTeam( TF_TEAM_BLUE ) || area->IsPotentiallyVisibleToTeam( TF_TEAM_RED ) )
+ {
+ continue;
+ }
+
+ ambushVector.AddToTail( area );
+ }
+ }
+
+ if ( ambushVector.Count() == 0 )
+ {
+ // no place to spawn the mob this time
+ return;
+ }
+
+ for( int retry=0; retry<10; ++retry )
+ {
+ int which = RandomInt( 0, ambushVector.Count()-1 );
+ m_zombieSpawnSpot = ambushVector[ which ]->GetCenter() + Vector( 0, 0, StepHeight );
+
+ if ( !IsSpaceToSpawnHere( m_zombieSpawnSpot ) )
+ {
+ continue;
+ }
+
+ // spawn a mob here
+ m_zombiesLeftToSpawn = tf_halloween_zombie_mob_spawn_count.GetInt();
+
+ break;
+ }
+ }
+}
+
+//---------------------------------------------------------------------------------------------------------
+static bool isBossForceSpawning = false;
+
+// force the boss to spawn where our cursor is pointing
+CON_COMMAND_F( tf_halloween_force_boss_spawn, "For testing.", FCVAR_CHEAT )
+{
+ isBossForceSpawning = true;
+}
+
+
+CON_COMMAND_F( cc_spawn_merasmus_at_level, "Force Merasmus to spawn at a specific difficulty level", FCVAR_CHEAT )
+{
+ if( args.ArgC() != 2 )
+ {
+ DevMsg( "Must specify a level\n" );
+ return;
+ }
+
+ CMerasmus::DBG_SetLevel( atoi(args[1]) );
+
+ tf_halloween_force_boss_spawn( args );
+}
+
+
+extern ConVar tf_halloween_bot_min_player_count;
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SpawnHalloweenBoss( void )
+{
+ if ( !IsHolidayActive( kHoliday_Halloween ) )
+ return;
+
+ // only spawn the Halloween Boss on our Halloween maps
+ HalloweenBossType bossType = HALLOWEEN_BOSS_INVALID;
+
+ float bossInterval = 0.0f;
+ float bossIntervalVariation = 0.0f;
+
+ HalloweenScenarioType scenario = GetHalloweenScenario();
+ if ( scenario == HALLOWEEN_SCENARIO_MANN_MANOR )
+ {
+ bossType = HALLOWEEN_BOSS_HHH;
+ bossInterval = tf_halloween_boss_spawn_interval.GetFloat();
+ bossIntervalVariation = tf_halloween_boss_spawn_interval_variation.GetFloat();
+
+ }
+ else if ( scenario == HALLOWEEN_SCENARIO_VIADUCT )
+ {
+ bossType = HALLOWEEN_BOSS_MONOCULUS;
+ bossInterval = tf_halloween_eyeball_boss_spawn_interval.GetFloat();
+ bossIntervalVariation = tf_halloween_eyeball_boss_spawn_interval_variation.GetFloat();
+ }
+ else if ( scenario == HALLOWEEN_SCENARIO_LAKESIDE )
+ {
+ bossType = HALLOWEEN_BOSS_MERASMUS;
+
+ if ( CMerasmus::GetMerasmusLevel() <= 3 )
+ {
+ bossInterval = tf_merasmus_spawn_interval.GetFloat();
+ bossIntervalVariation = tf_merasmus_spawn_interval_variation.GetFloat();
+ }
+ else
+ {
+ // after level 3, spawn Merasmus every 60 secs
+ bossInterval = 60;
+ bossIntervalVariation = 0;
+ }
+
+ // check if the wheel is still spinning
+ CWheelOfDoom* pWheel = assert_cast< CWheelOfDoom* >( gEntList.FindEntityByClassname( NULL, "wheel_of_doom" ) );
+ if ( pWheel && !pWheel->IsDoneBoardcastingEffectSound() )
+ {
+ return;
+ }
+ }
+ //else if ( scenario == HALLOWEEN_SCENARIO_HIGHTOWER )
+ //{
+ // bool bWasEnabled = tf_halloween_zombie_mob_enabled.GetBool();
+ // tf_halloween_zombie_mob_enabled.SetValue( true );
+
+ // // not a boss battle map
+ // SpawnZombieMob();
+
+ // tf_halloween_zombie_mob_enabled.SetValue( bWasEnabled );
+
+ // bossType = "eyeball_boss";
+ // bossInterval = tf_halloween_eyeball_boss_spawn_interval.GetFloat();
+ // bossIntervalVariation = tf_halloween_eyeball_boss_spawn_interval_variation.GetFloat();
+ //}
+ else
+ {
+ // not a boss battle map
+ SpawnZombieMob();
+
+ return;
+ }
+
+ // only one boss at a time
+ if ( GetActiveBoss() )
+ {
+ // boss is still out there - restart the timer
+ StartHalloweenBossTimer( bossInterval, bossIntervalVariation );
+ isBossForceSpawning = false;
+ return;
+ }
+
+ if ( !m_halloweenBossTimer.IsElapsed() && !isBossForceSpawning )
+ return;
+
+ // boss timer has elapsed
+ if ( m_halloweenBossTimer.HasStarted() || isBossForceSpawning )
+ {
+ if ( !isBossForceSpawning )
+ {
+ // timer was started and has elapsed - time to spawn the boss
+ if ( InSetup() || IsInWaitingForPlayers() )
+ return;
+
+ // require a minimum number of human players in the game before the boss appears
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS );
+ CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS );
+
+ // only count humans
+ int totalPlayers = 0;
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ if ( !playerVector[i]->IsBot() )
+ {
+ ++totalPlayers;
+ }
+ }
+
+ if ( totalPlayers < tf_halloween_bot_min_player_count.GetInt() )
+ return;
+ }
+
+ Vector bossSpawnPos = vec3_origin;
+
+ // spawn on the currently contested point
+ CTeamControlPoint *contestedPoint = NULL;
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( pMaster )
+ {
+ for( int i=0; i<pMaster->GetNumPoints(); ++i )
+ {
+ contestedPoint = pMaster->GetControlPoint( i );
+ if ( contestedPoint && pMaster->IsInRound( contestedPoint ) )
+ {
+ if ( ObjectiveResource()->GetOwningTeam( contestedPoint->GetPointIndex() ) == TF_TEAM_BLUE )
+ continue;
+
+ // blue are the invaders
+ if ( !TeamplayGameRules()->TeamMayCapturePoint( TF_TEAM_BLUE, contestedPoint->GetPointIndex() ) )
+ continue;
+
+ break;
+ }
+ }
+ }
+
+ CBaseEntity *pCustomSpawnBossPos = gEntList.FindEntityByClassname( NULL, "spawn_boss" );
+ if ( pCustomSpawnBossPos )
+ {
+ bossSpawnPos = pCustomSpawnBossPos->GetAbsOrigin();
+ }
+ else if ( contestedPoint )
+ {
+ bossSpawnPos = contestedPoint->GetAbsOrigin();
+
+ if ( scenario == HALLOWEEN_SCENARIO_VIADUCT || scenario == HALLOWEEN_SCENARIO_LAKESIDE )
+ {
+ // revert ownership of point to neutral
+ contestedPoint->ForceOwner( 0 );
+
+ // pause the timers
+ if ( IsInKothMode() )
+ {
+ variant_t sVariant;
+ CTeamRoundTimer *pTimer = GetKothTeamTimer( TF_TEAM_BLUE );
+ if ( pTimer )
+ {
+ pTimer->AcceptInput( "Pause", NULL, NULL, sVariant, 0 );
+ }
+
+ pTimer = GetKothTeamTimer( TF_TEAM_RED );
+ if ( pTimer )
+ {
+ pTimer->AcceptInput( "Pause", NULL, NULL, sVariant, 0 );
+ }
+ }
+ }
+ }
+ else
+ {
+ // pick a random spot
+ CUtlVector< CTFNavArea * > spawnAreaVector;
+ for( int i=0; i<TheNavAreas.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)TheNavAreas[i];
+
+ if ( area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED | TF_NAV_SPAWN_ROOM_EXIT ) )
+ {
+ // don't spawn in team spawn rooms
+ continue;
+ }
+
+ // don't use small nav areas
+ const float goodSize = 100.0f;
+ if ( area->GetSizeX() < goodSize || area->GetSizeY() < goodSize )
+ {
+ continue;
+ }
+
+ spawnAreaVector.AddToTail( area );
+ }
+
+ if ( spawnAreaVector.Count() == 0 )
+ {
+ // no place to spawn (!)
+ return;
+ }
+
+ int which = RandomInt( 0, spawnAreaVector.Count()-1 );
+ bossSpawnPos = spawnAreaVector[ which ]->GetCenter();
+ }
+
+ CHalloweenBaseBoss::SpawnBossAtPos( bossType, bossSpawnPos );
+
+ // pick next spawn time
+ StartHalloweenBossTimer( bossInterval, bossIntervalVariation );
+
+ isBossForceSpawning = false;
+ }
+ else
+ {
+ // Merasmus has a more reliable initial spawn time
+ if( IsHalloweenScenario( HALLOWEEN_SCENARIO_LAKESIDE ) )
+ {
+ StartHalloweenBossTimer( bossInterval, bossIntervalVariation );
+ }
+ else
+ {
+ // initial spawn time
+ m_halloweenBossTimer.Start( 0.5f * RandomFloat( 0.0f, bossInterval + bossIntervalVariation ) );
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::BeginHaunting( int nDesiredCount, float flMinDuration, float flMaxDuration )
+{
+ if ( !IsHolidayActive( kHoliday_Halloween ) )
+ return;
+
+ if ( !IsHalloweenScenario( HALLOWEEN_SCENARIO_VIADUCT ) && !IsHalloweenScenario( HALLOWEEN_SCENARIO_LAKESIDE ) )
+ {
+ CTFHolidayEntity *pHolidayEntity = dynamic_cast<CTFHolidayEntity*> ( gEntList.FindEntityByClassname( NULL, "tf_logic_holiday" ) );
+ if ( !pHolidayEntity || !pHolidayEntity->ShouldAllowHaunting() )
+ return;
+ }
+
+ const int desiredGhostCount = nDesiredCount;
+
+ // if there are existing ghosts, extend their time
+ CUtlVector< CGhost * > priorGhostVector;
+ for( int g=0; g<m_ghostVector.Count(); ++g )
+ {
+ if ( m_ghostVector[g] != NULL )
+ {
+ priorGhostVector.AddToTail( m_ghostVector[g] );
+ }
+ }
+
+ m_ghostVector.RemoveAll();
+
+ for( int g=0; g<priorGhostVector.Count(); ++g )
+ {
+ priorGhostVector[g]->SetLifetime( RandomFloat( flMinDuration, flMaxDuration ) );
+
+ m_ghostVector.AddToTail( priorGhostVector[g] );
+ }
+
+ if ( m_ghostVector.Count() >= desiredGhostCount )
+ return;
+
+ // spawn ghosts away from players
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS );
+ CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS );
+
+ const int ghostCount = desiredGhostCount - m_ghostVector.Count();
+
+ CUtlVector< Vector > spawnVector;
+
+ for( int i=0; i<TheNavAreas.Count(); ++i )
+ {
+ CTFNavArea *area = (CTFNavArea *)TheNavAreas.Element(i);
+
+ if ( area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) )
+ {
+ // keep out of spawn rooms
+ continue;
+ }
+
+ Vector spot = area->GetCenter();
+
+ // don't spawn near players (so they aren't instantly scared)
+ int p;
+ for( p=0; p<playerVector.Count(); ++p )
+ {
+ if ( ( playerVector[p]->GetAbsOrigin() - spot ).IsLengthLessThan( 1.25f * GHOST_SCARE_RADIUS ) )
+ {
+ break;
+ }
+ }
+
+ if ( p == playerVector.Count() )
+ {
+ spawnVector.AddToTail( spot );
+ }
+ }
+
+ if ( spawnVector.Count() == 0 )
+ {
+ return;
+ }
+
+ for( int g=0; g<ghostCount; ++g )
+ {
+ int which = RandomInt( 0, spawnVector.Count()-1 );
+
+ CGhost *ghost = SpawnGhost( spawnVector[ which ], vec3_angle, RandomFloat( flMinDuration, flMaxDuration ) );
+
+ m_ghostVector.AddToTail( ghost );
+ }
+}
+
+static const int k_RecentPlayerInfoMaxTime = 7200; // 2 hours
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::PlayerHistory_AddPlayer( CTFPlayer *pTFPlayer )
+{
+ if ( !pTFPlayer )
+ return;
+
+ CSteamID steamID;
+ pTFPlayer->GetSteamID( &steamID );
+ if ( !steamID.IsValid() || !steamID.BIndividualAccount() )
+ return;
+
+ // Exists?
+ FOR_EACH_VEC_BACK( m_vecPlayerHistory, i )
+ {
+ if ( m_vecPlayerHistory[i].steamID == steamID )
+ {
+ m_vecPlayerHistory[i].flTime = Plat_FloatTime();
+ return;
+ }
+
+ // Do maintenance here.
+ if ( Plat_FloatTime() - m_vecPlayerHistory[i].flTime >= (float)k_RecentPlayerInfoMaxTime )
+ {
+ m_vecPlayerHistory.Remove( i );
+ }
+ }
+
+ PlayerHistoryInfo_t info =
+ {
+ steamID,
+ (float) Plat_FloatTime(),
+ pTFPlayer->GetTeamNumber()
+ };
+ m_vecPlayerHistory.AddToTail( info );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+PlayerHistoryInfo_t *CTFGameRules::PlayerHistory_GetPlayerInfo( CTFPlayer *pTFPlayer )
+{
+ if ( !pTFPlayer )
+ return NULL;
+
+ CSteamID steamID;
+ pTFPlayer->GetSteamID( &steamID );
+ if ( !steamID.IsValid() || !steamID.BIndividualAccount() )
+ return NULL;
+
+ FOR_EACH_VEC_BACK( m_vecPlayerHistory, i )
+ {
+ if ( m_vecPlayerHistory[i].steamID == steamID )
+ return &m_vecPlayerHistory[i];
+ }
+
+ return NULL;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Returns -1 if we don't have the data
+//-----------------------------------------------------------------------------
+int CTFGameRules::PlayerHistory_GetTimeSinceLastSeen( CTFPlayer *pTFPlayer )
+{
+ PlayerHistoryInfo_t *pInfo = PlayerHistory_GetPlayerInfo( pTFPlayer );
+ if ( !pInfo )
+ return -1;
+
+ // We only care about whole seconds
+ return (int)( Plat_FloatTime() - pInfo->flTime );
+}
+#endif
+
+//=============================================================================
+// HPE_BEGIN:
+// [msmith] TEMP CODE: needs to be replaced with the final solution for our training mission navigation.
+//=============================================================================
+void CTFGameRules::LoadNextTrainingMap()
+{
+ if ( m_hTrainingModeLogic )
+ {
+ g_fGameOver = true;
+ const char* pNextMap = m_hTrainingModeLogic->GetNextMap();
+ if ( pNextMap && FStrEq( pNextMap, "" ) == false )
+ {
+ Msg( "CHANGE LEVEL: %s\n", pNextMap );
+ engine->ChangeLevel( pNextMap, NULL );
+ }
+ else
+ {
+ Msg( "CHANGE LEVEL: %s\n", STRING( gpGlobals->mapname ) );
+ engine->ChangeLevel( STRING( gpGlobals->mapname ), NULL );
+ }
+ return;
+ }
+
+ Msg( "CHANGE LEVEL: %s\n", STRING( gpGlobals->mapname ) );
+ engine->ChangeLevel( STRING( gpGlobals->mapname ), NULL );
+}
+//=============================================================================
+// HPE_END
+//=============================================================================
+
+//Runs think for all player's conditions
+//Need to do this here instead of the player so players that crash still run their important thinks
+void CTFGameRules::RunPlayerConditionThink ( void )
+{
+ for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( pPlayer )
+ {
+ pPlayer->m_Shared.ConditionGameRulesThink();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::FrameUpdatePostEntityThink()
+{
+ BaseClass::FrameUpdatePostEntityThink();
+
+ RunPlayerConditionThink();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CheckCapsPerRound()
+{
+ return IsPasstimeMode()
+ ? SetPasstimeWinningTeam()
+ : SetCtfWinningTeam();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::SetPasstimeWinningTeam()
+{
+ Assert( IsPasstimeMode() );
+
+ int iScoreLimit = tf_passtime_scores_per_round.GetInt();
+ if ( iScoreLimit <= 0 )
+ {
+ // no score limit set, play forever
+ return false;
+ }
+
+ // TODO need to generalize the "flag captures" parts of CTFTeam to avoid
+ // this confusing overload of the flag captures concept, or maybe defer
+ // this logic to the specific object that manages the mode.
+ CTFTeamManager *pTeamMgr = TFTeamMgr();
+ int iBlueScore = pTeamMgr->GetFlagCaptures( TF_TEAM_BLUE );
+ int iRedScore = pTeamMgr->GetFlagCaptures( TF_TEAM_RED );
+ if ( ( iBlueScore < iScoreLimit ) && ( iRedScore < iScoreLimit ) )
+ {
+ // no team has exceeded the score limit
+ return false;
+ }
+
+ int iWinnerTeam = ( iBlueScore > iRedScore )
+ ? TF_TEAM_BLUE
+ : TF_TEAM_RED;
+ SetWinningTeam( iWinnerTeam, WINREASON_SCORED );
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::SetCtfWinningTeam()
+{
+ Assert( !IsPasstimeMode() );
+ if ( tf_flag_caps_per_round.GetInt() > 0 )
+ {
+ int iMaxCaps = -1;
+ CTFTeam *pMaxTeam = NULL;
+
+ // check to see if any team has won a "round"
+ int nTeamCount = TFTeamMgr()->GetTeamCount();
+ for ( int iTeam = FIRST_GAME_TEAM; iTeam < nTeamCount; ++iTeam )
+ {
+ CTFTeam *pTeam = GetGlobalTFTeam( iTeam );
+ if ( !pTeam )
+ continue;
+
+ // we might have more than one team over the caps limit (if the server op lowered the limit)
+ // so loop through to see who has the most among teams over the limit
+ if ( pTeam->GetFlagCaptures() >= tf_flag_caps_per_round.GetInt() )
+ {
+ if ( pTeam->GetFlagCaptures() > iMaxCaps )
+ {
+ iMaxCaps = pTeam->GetFlagCaptures();
+ pMaxTeam = pTeam;
+ }
+ }
+ }
+
+ if ( iMaxCaps != -1 && pMaxTeam != NULL )
+ {
+ SetWinningTeam( pMaxTeam->GetTeamNumber(), WINREASON_FLAG_CAPTURE_LIMIT );
+ return true;
+ }
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CheckWinLimit( bool bAllowEnd /*= true*/, int nAddValueWhenChecking /*= 0*/ )
+{
+ if ( IsInPreMatch() )
+ return false;
+
+ bool bWinner = false;
+ int iTeam = TEAM_UNASSIGNED;
+ int iReason = WINREASON_NONE;
+ const char *pszReason = "";
+
+ if ( mp_winlimit.GetInt() != 0 )
+ {
+ if ( ( TFTeamMgr()->GetTeam( TF_TEAM_BLUE )->GetScore() + nAddValueWhenChecking ) >= mp_winlimit.GetInt() )
+ {
+ pszReason = "Team \"BLUE\" triggered \"Intermission_Win_Limit\"\n";
+ bWinner = true;
+ iTeam = TF_TEAM_BLUE;
+ iReason = WINREASON_WINLIMIT;
+ }
+ else if ( ( TFTeamMgr()->GetTeam( TF_TEAM_RED )->GetScore() + nAddValueWhenChecking ) >= mp_winlimit.GetInt() )
+ {
+ pszReason = "Team \"RED\" triggered \"Intermission_Win_Limit\"\n";
+ bWinner = true;
+ iTeam = TF_TEAM_RED;
+ iReason = WINREASON_WINLIMIT;
+ }
+ }
+
+ // has one team go far enough ahead of the other team to trigger the win difference?
+ if ( !bWinner )
+ {
+ int iWinLimit = mp_windifference.GetInt();
+ if ( iWinLimit > 0 )
+ {
+ int iBlueScore = TFTeamMgr()->GetTeam( TF_TEAM_BLUE )->GetScore();
+ int iRedScore = TFTeamMgr()->GetTeam( TF_TEAM_RED )->GetScore();
+
+ if ( (iBlueScore - iRedScore) >= iWinLimit )
+ {
+ if ( (mp_windifference_min.GetInt() == 0) || (iBlueScore >= mp_windifference_min.GetInt()) )
+ {
+ pszReason = "Team \"BLUE\" triggered \"Intermission_Win_Limit\" due to mp_windifference\n";
+ bWinner = true;
+ iTeam = TF_TEAM_BLUE;
+ iReason = WINREASON_WINDIFFLIMIT;
+ }
+ }
+ else if ( (iRedScore - iBlueScore) >= iWinLimit )
+ {
+ if ( (mp_windifference_min.GetInt() == 0) || (iRedScore >= mp_windifference_min.GetInt()) )
+ {
+ pszReason = "Team \"RED\" triggered \"Intermission_Win_Limit\" due to mp_windifference\n";
+ bWinner = true;
+ iTeam = TF_TEAM_RED;
+ iReason = WINREASON_WINDIFFLIMIT;
+ }
+ }
+ }
+ }
+
+ if ( bWinner )
+ {
+ if ( bAllowEnd )
+ {
+ IGameEvent *event = gameeventmanager->CreateEvent( "tf_game_over" );
+ if ( event )
+ {
+ if ( iReason == WINREASON_WINDIFFLIMIT )
+ {
+ event->SetString( "reason", "Reached Win Difference Limit" );
+ }
+ else
+ {
+ event->SetString( "reason", "Reached Win Limit" );
+ }
+ gameeventmanager->FireEvent( event );
+ }
+
+ if ( IsInTournamentMode() == true )
+ {
+ SetWinningTeam( iTeam, iReason, true, false, true );
+ }
+ else
+ {
+ GoToIntermission();
+ }
+
+ Assert( V_strlen( pszReason ) );
+ UTIL_LogPrintf( "%s", pszReason );
+ }
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::CheckRespawnWaves()
+{
+ BaseClass::CheckRespawnWaves();
+
+ // Look for overrides
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( !pTFPlayer )
+ continue;
+
+ if ( pTFPlayer->IsAlive() )
+ continue;
+
+ if ( m_iRoundState == GR_STATE_PREROUND )
+ continue;
+
+ // Triggers can force a player to spawn at a specific time
+ if ( pTFPlayer->GetRespawnTimeOverride() != -1.f &&
+ gpGlobals->curtime > pTFPlayer->GetDeathTime() + pTFPlayer->GetRespawnTimeOverride() )
+ {
+ pTFPlayer->ForceRespawn();
+ }
+ else if ( IsPVEModeActive() )
+ {
+ // special stuff for PVE mode
+ if ( !ShouldRespawnQuickly( pTFPlayer ) )
+ continue;
+
+ // If the player hasn't been dead the minimum respawn time, he
+ // waits until the next wave.
+ if ( !HasPassedMinRespawnTime( pTFPlayer ) )
+ continue;
+
+ if ( !pTFPlayer->IsReadyToSpawn() )
+ {
+ // Let the player spawn immediately when they do pick a class
+ if ( pTFPlayer->ShouldGainInstantSpawn() )
+ {
+ pTFPlayer->AllowInstantSpawn();
+ }
+ continue;
+ }
+
+ // Respawn this player
+ pTFPlayer->ForceRespawn();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::PlayWinSong( int team )
+{
+ if ( IsPlayingSpecialDeliveryMode() )
+ return;
+
+ bool bGameOver = IsGameOver();
+
+ if ( !IsInStopWatch() || bGameOver )
+ {
+ // Give the match a chance to play something custom. It returns true if it handled everything
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc && pMatchDesc->BPlayWinMusic( team, bGameOver ) )
+ {
+ return;
+ }
+ }
+
+ if ( IsInTournamentMode() && IsInStopWatch() && ObjectiveResource() )
+ {
+ int iStopWatchTimer = ObjectiveResource()->GetStopWatchTimer();
+ CTeamRoundTimer *pStopWatch = dynamic_cast< CTeamRoundTimer* >( UTIL_EntityByIndex( iStopWatchTimer ) );
+ if ( ( pStopWatch && pStopWatch->IsWatchingTimeStamps() ) || ( !m_bForceMapReset ) )
+ {
+ BroadcastSound( 255, "MatchMaking.RoundEndStalemateMusic" );
+ return;
+ }
+ }
+
+ CTeamplayRoundBasedRules::PlayWinSong( team );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetWinningTeam( int team, int iWinReason, bool bForceMapReset /* = true */, bool bSwitchTeams /* = false*/, bool bDontAddScore /* = false*/, bool bFinal /*= false*/ )
+{
+ // matching the value calculated in CTeamplayRoundBasedRules::State_Enter_TEAM_WIN()
+ // for m_flStateTransitionTime and adding 1 second to make sure we're covered
+ int nTime = GetBonusRoundTime( bFinal ) + 1;
+
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( !pTFPlayer )
+ continue;
+
+ // store our team for the response rules at the next round start
+ // (teams might be switched for attack/defend maps)
+ pTFPlayer->SetPrevRoundTeamNum( pTFPlayer->GetTeamNumber() );
+
+ if ( team != TEAM_UNASSIGNED )
+ {
+ if ( pTFPlayer->GetTeamNumber() == team )
+ {
+ if ( pTFPlayer->IsAlive() )
+ {
+ pTFPlayer->m_Shared.AddCond( TF_COND_CRITBOOSTED_BONUS_TIME, nTime );
+ }
+ }
+ else
+ {
+ pTFPlayer->ClearExpression();
+#ifdef GAME_DLL
+ // Loser karts get max Damage and stun
+ if ( pTFPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_KART ) )
+ {
+ pTFPlayer->AddKartDamage( 666 );
+ pTFPlayer->m_Shared.StunPlayer( 1.5f, 1.0f, TF_STUN_BOTH ); // Short full stun then slow
+ pTFPlayer->m_Shared.StunPlayer( 10.0f, 0.25f, TF_STUN_MOVEMENT );
+ }
+#endif //GAME_DLL
+ }
+ }
+ }
+
+ DuelMiniGame_AssignWinners();
+
+#ifdef TF_RAID_MODE
+ if ( !IsBossBattleMode() )
+ {
+ // Don't do a full reset in Raid mode if the defending team didn't win
+ if ( IsRaidMode() && team != TF_TEAM_PVE_DEFENDERS )
+ {
+ bForceMapReset = false;
+ }
+ }
+#endif // TF_RAID_MODE
+
+ SetBirthdayPlayer( NULL );
+
+#ifdef GAME_DLL
+ if ( m_bPlayingKoth )
+ {
+ // Increment BLUE KOTH cap time
+ CTeamRoundTimer *pKOTHTimer = TFGameRules()->GetBlueKothRoundTimer();
+ GetGlobalTFTeam( TF_TEAM_BLUE )->AddKOTHTime( pKOTHTimer->GetTimerMaxLength() - pKOTHTimer->GetTimeRemaining() );
+
+ // Increment RED KOTH cap time
+ pKOTHTimer = TFGameRules()->GetRedKothRoundTimer();
+ GetGlobalTFTeam( TF_TEAM_RED )->AddKOTHTime( pKOTHTimer->GetTimerMaxLength() - pKOTHTimer->GetTimeRemaining() );
+ }
+ else if ( HasMultipleTrains() )
+ {
+ for ( int i = 0 ; i < ITFTeamTrainWatcher::AutoList().Count() ; ++i )
+ {
+ CTeamTrainWatcher *pTrainWatcher = static_cast< CTeamTrainWatcher* >( ITFTeamTrainWatcher::AutoList()[i] );
+ if ( !pTrainWatcher->IsDisabled() )
+ {
+ if ( pTrainWatcher->GetTeamNumber() == TF_TEAM_RED )
+ {
+ GetGlobalTFTeam( TF_TEAM_RED )->AddPLRTrack( pTrainWatcher->GetTrainProgress() );
+ }
+ else
+ {
+ GetGlobalTFTeam( TF_TEAM_BLUE )->AddPLRTrack( pTrainWatcher->GetTrainProgress() );
+ }
+ }
+ }
+ }
+#endif
+
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_DOOMSDAY ) && CTFMinigameLogic::GetMinigameLogic() )
+ {
+ CTFMiniGame *pMiniGame = CTFMinigameLogic::GetMinigameLogic()->GetActiveMinigame();
+ if ( pMiniGame )
+ {
+ IGameEvent *event = gameeventmanager->CreateEvent( "minigame_win" );
+ if ( event )
+ {
+ event->SetInt( "team", team );
+ event->SetInt( "type", (int)( pMiniGame->GetMinigameType() ) );
+ gameeventmanager->FireEvent( event );
+ }
+ }
+ }
+
+ if ( IsPasstimeMode() )
+ {
+ CTF_GameStats.m_passtimeStats.summary.nRoundEndReason = iWinReason;
+ CTF_GameStats.m_passtimeStats.summary.nRoundRemainingSec = (int) GetActiveRoundTimer()->GetTimeRemaining();
+ CTF_GameStats.m_passtimeStats.summary.nScoreBlue = GetGlobalTFTeam( TF_TEAM_BLUE )->GetFlagCaptures();
+ CTF_GameStats.m_passtimeStats.summary.nScoreRed = GetGlobalTFTeam( TF_TEAM_RED )->GetFlagCaptures();
+
+ // stats reporting happens as a result of BaseClass::SetWinningTeam, but we need to make sure to
+ // update ball carry data before stats are reported.
+ // FIXME: refactor this so we're not calling it just for its side effects :/
+ CPasstimeBall *pBall = g_pPasstimeLogic->GetBall();
+ if ( pBall )
+ {
+ pBall->SetStateOutOfPlay();
+ }
+ }
+
+ CTeamplayRoundBasedRules::SetWinningTeam( team, iWinReason, bForceMapReset, bSwitchTeams, bDontAddScore, bFinal );
+
+ if ( IsCompetitiveMode() )
+ {
+ HaveAllPlayersSpeakConceptIfAllowed( IsGameOver() ? MP_CONCEPT_MATCH_OVER_COMP : MP_CONCEPT_GAME_OVER_COMP );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetStalemate( int iReason, bool bForceMapReset /* = true */, bool bSwitchTeams /* = false */ )
+{
+ DuelMiniGame_AssignWinners();
+
+ if ( IsPasstimeMode() )
+ {
+ CTF_GameStats.m_passtimeStats.summary.bStalemate = true;
+ CTF_GameStats.m_passtimeStats.summary.bSuddenDeath = mp_stalemate_enable.GetBool();
+ CTF_GameStats.m_passtimeStats.summary.bMeleeOnlySuddenDeath = mp_stalemate_meleeonly.GetBool();
+ }
+
+ BaseClass::SetStalemate( iReason, bForceMapReset, bSwitchTeams );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+float CTFGameRules::GetPreMatchEndTime() const
+{
+ //TFTODO: implement this.
+ return gpGlobals->curtime;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::GoToIntermission( void )
+{
+ // Tell the clients to recalculate the holiday
+ IGameEvent *event = gameeventmanager->CreateEvent( "recalculate_holidays" );
+ if ( event )
+ {
+ gameeventmanager->FireEvent( event );
+ }
+
+ UTIL_CalculateHolidays();
+
+ BaseClass::GoToIntermission();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::RecalculateTruce( void )
+{
+ bool bTruceActive = false;
+
+ // Call a truce if the teams are fighting a Halloween boss
+ if ( IsHolidayActive( kHoliday_Halloween ) )
+ {
+ if ( ( IMerasmusAutoList::AutoList().Count() > 0 ) || ( IEyeballBossAutoList::AutoList().Count() > 0 ) )
+ {
+ bool bHaveActiveBoss = false;
+
+ for ( int i = 0; i < IMerasmusAutoList::AutoList().Count(); ++i )
+ {
+ CMerasmus *pBoss = static_cast< CMerasmus* >( IMerasmusAutoList::AutoList()[i] );
+ if ( !pBoss->IsMarkedForDeletion() )
+ {
+ bHaveActiveBoss = true;
+ }
+ }
+
+ for ( int i = 0; i < IEyeballBossAutoList::AutoList().Count(); ++i )
+ {
+ CEyeballBoss *pBoss = static_cast< CEyeballBoss* >( IEyeballBossAutoList::AutoList()[i] );
+ if ( !pBoss->IsMarkedForDeletion() )
+ {
+ if ( ( pBoss->GetTeamNumber() != TF_TEAM_RED ) && ( pBoss->GetTeamNumber() != TF_TEAM_BLUE ) )
+ {
+ bHaveActiveBoss = true;
+ }
+ }
+ }
+
+ if ( bHaveActiveBoss && ( IsValveMap() || tf_halloween_allow_truce_during_boss_event.GetBool() || IsMapForcedTruceDuringBossFight() ) )
+ {
+ bTruceActive = true;
+ }
+ }
+ }
+
+#ifdef STAGING_ONLY
+ if ( tf_truce.GetBool() )
+ {
+ bTruceActive = true;
+ }
+#endif
+
+ if ( m_bTruceActive != bTruceActive )
+ {
+ m_bTruceActive.Set( bTruceActive );
+
+ CReliableBroadcastRecipientFilter filter;
+ if ( m_bTruceActive )
+ {
+ SendHudNotification( filter, HUD_NOTIFY_TRUCE_START, true );
+ if ( m_hGamerulesProxy )
+ {
+ m_hGamerulesProxy->TruceStart();
+ }
+ }
+ else
+ {
+ SendHudNotification( filter, HUD_NOTIFY_TRUCE_END, true );
+ if ( m_hGamerulesProxy )
+ {
+ m_hGamerulesProxy->TruceEnd();
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::FPlayerCanTakeDamage( CBasePlayer *pPlayer, CBaseEntity *pAttacker, const CTakeDamageInfo &info )
+{
+ // guard against NULL pointers if players disconnect
+ if ( !pPlayer || !pAttacker )
+ return false;
+
+ if ( IsTruceActive() && ( pPlayer != pAttacker ) && ( pPlayer->GetTeamNumber() != pAttacker->GetTeamNumber() ) )
+ {
+ if ( ( ( pAttacker->GetTeamNumber() == TF_TEAM_RED ) && ( pPlayer->GetTeamNumber() == TF_TEAM_BLUE ) ) || ( ( pAttacker->GetTeamNumber() == TF_TEAM_BLUE ) && ( pPlayer->GetTeamNumber() == TF_TEAM_RED ) ) )
+ {
+ CBaseEntity *pInflictor = info.GetInflictor();
+ if ( pInflictor )
+ {
+ return !( pInflictor->IsTruceValidForEnt() || pAttacker->IsTruceValidForEnt() );
+ }
+ else
+ {
+ return !pAttacker->IsTruceValidForEnt();
+ }
+ }
+ }
+
+ // if pAttacker is an object, we can only do damage if pPlayer is our builder
+ if ( pAttacker->IsBaseObject() )
+ {
+ CBaseObject *pObj = ( CBaseObject *)pAttacker;
+
+ if ( pObj->GetBuilder() == pPlayer || pPlayer->GetTeamNumber() != pObj->GetTeamNumber() )
+ {
+ // Builder and enemies
+ return true;
+ }
+ else
+ {
+ // Teammates of the builder
+ return false;
+ }
+ }
+
+ // prevent eyeball rockets from hurting teammates if it's a spell
+ if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SPELL_MONOCULUS && pAttacker->GetTeamNumber() == pPlayer->GetTeamNumber() )
+ {
+ return false;
+ }
+
+ // in PvE modes, if entities are on the same team, they can't hurt each other
+ // this is needed since not all entities will be players
+ if ( IsPVEModeActive() &&
+ pPlayer->GetTeamNumber() == pAttacker->GetTeamNumber() &&
+ pPlayer != pAttacker &&
+ !info.IsForceFriendlyFire() )
+ {
+ return false;
+ }
+
+ return BaseClass::FPlayerCanTakeDamage( pPlayer, pAttacker, info );
+}
+
+Vector DropToGround(
+ CBaseEntity *pMainEnt,
+ const Vector &vPos,
+ const Vector &vMins,
+ const Vector &vMaxs )
+{
+ trace_t trace;
+ UTIL_TraceHull( vPos, vPos + Vector( 0, 0, -500 ), vMins, vMaxs, MASK_SOLID, pMainEnt, COLLISION_GROUP_NONE, &trace );
+ return trace.endpos;
+}
+
+
+void TestSpawnPointType( const char *pEntClassName )
+{
+ // Find the next spawn spot.
+ CBaseEntity *pSpot = gEntList.FindEntityByClassname( NULL, pEntClassName );
+
+ while( pSpot )
+ {
+ // trace a box here
+ Vector vTestMins = pSpot->GetAbsOrigin() + VEC_HULL_MIN;
+ Vector vTestMaxs = pSpot->GetAbsOrigin() + VEC_HULL_MAX;
+
+ if ( UTIL_IsSpaceEmpty( pSpot, vTestMins, vTestMaxs ) )
+ {
+ // the successful spawn point's location
+ NDebugOverlay::Box( pSpot->GetAbsOrigin(), VEC_HULL_MIN, VEC_HULL_MAX, 0, 255, 0, 100, 60 );
+
+ // drop down to ground
+ Vector GroundPos = DropToGround( NULL, pSpot->GetAbsOrigin(), VEC_HULL_MIN, VEC_HULL_MAX );
+
+ // the location the player will spawn at
+ NDebugOverlay::Box( GroundPos, VEC_HULL_MIN, VEC_HULL_MAX, 0, 0, 255, 100, 60 );
+
+ // draw the spawn angles
+ QAngle spotAngles = pSpot->GetLocalAngles();
+ Vector vecForward;
+ AngleVectors( spotAngles, &vecForward );
+ NDebugOverlay::HorzArrow( pSpot->GetAbsOrigin(), pSpot->GetAbsOrigin() + vecForward * 32, 10, 255, 0, 0, 255, true, 60 );
+ }
+ else
+ {
+ // failed spawn point location
+ NDebugOverlay::Box( pSpot->GetAbsOrigin(), VEC_HULL_MIN, VEC_HULL_MAX, 255, 0, 0, 100, 60 );
+ }
+
+ // increment pSpot
+ pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName );
+ }
+}
+
+// -------------------------------------------------------------------------------- //
+
+void TestSpawns()
+{
+ TestSpawnPointType( "info_player_teamspawn" );
+}
+ConCommand cc_TestSpawns( "map_showspawnpoints", TestSpawns, "Dev - test the spawn points, draws for 60 seconds", FCVAR_CHEAT );
+
+
+// -------------------------------------------------------------------------------- //
+
+void cc_ShowRespawnTimes()
+{
+ CTFGameRules *pRules = TFGameRules();
+ CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() );
+
+ if ( pRules && pPlayer )
+ {
+ float flRedMin = ( pRules->m_TeamRespawnWaveTimes[TF_TEAM_RED] >= 0 ? pRules->m_TeamRespawnWaveTimes[TF_TEAM_RED] : mp_respawnwavetime.GetFloat() );
+ float flRedScalar = pRules->GetRespawnTimeScalar( TF_TEAM_RED );
+ float flNextRedRespawn = pRules->GetNextRespawnWave( TF_TEAM_RED, NULL ) - gpGlobals->curtime;
+
+ float flBlueMin = ( pRules->m_TeamRespawnWaveTimes[TF_TEAM_BLUE] >= 0 ? pRules->m_TeamRespawnWaveTimes[TF_TEAM_BLUE] : mp_respawnwavetime.GetFloat() );
+ float flBlueScalar = pRules->GetRespawnTimeScalar( TF_TEAM_BLUE );
+ float flNextBlueRespawn = pRules->GetNextRespawnWave( TF_TEAM_BLUE, NULL ) - gpGlobals->curtime;
+
+ char tempRed[128];
+ Q_snprintf( tempRed, sizeof( tempRed ), "Red: Min Spawn %2.2f, Scalar %2.2f, Next Spawn In: %.2f\n", flRedMin, flRedScalar, flNextRedRespawn );
+
+ char tempBlue[128];
+ Q_snprintf( tempBlue, sizeof( tempBlue ), "Blue: Min Spawn %2.2f, Scalar %2.2f, Next Spawn In: %.2f\n", flBlueMin, flBlueScalar, flNextBlueRespawn );
+
+ ClientPrint( pPlayer, HUD_PRINTTALK, tempRed );
+ ClientPrint( pPlayer, HUD_PRINTTALK, tempBlue );
+ }
+}
+
+ConCommand mp_showrespawntimes( "mp_showrespawntimes", cc_ShowRespawnTimes, "Show the min respawn times for the teams", FCVAR_CHEAT );
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+CBaseEntity *CTFGameRules::GetPlayerSpawnSpot( CBasePlayer *pPlayer )
+{
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+ if ( IsInItemTestingMode() && pTFPlayer->m_bItemTestingRespawn )
+ {
+ pTFPlayer->m_bItemTestingRespawn = false;
+ return NULL;
+ }
+
+ // get valid spawn point
+ CBaseEntity *pSpawnSpot = pPlayer->EntSelectSpawnPoint();
+
+ // drop down to ground
+ Vector GroundPos = DropToGround( pPlayer, pSpawnSpot->GetAbsOrigin(), VEC_HULL_MIN_SCALED( pTFPlayer ), VEC_HULL_MAX_SCALED( pTFPlayer ) );
+
+ // Move the player to the place it said.
+ pPlayer->SetLocalOrigin( GroundPos + Vector(0,0,1) );
+ pPlayer->SetAbsVelocity( vec3_origin );
+ pPlayer->SetLocalAngles( pSpawnSpot->GetLocalAngles() );
+ pPlayer->m_Local.m_vecPunchAngle = vec3_angle;
+ pPlayer->m_Local.m_vecPunchAngleVel = vec3_angle;
+ pPlayer->SnapEyeAngles( pSpawnSpot->GetLocalAngles() );
+
+ return pSpawnSpot;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Checks to see if the player is on the correct team and whether or
+// not the spawn point is available.
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsSpawnPointValid( CBaseEntity *pSpot, CBasePlayer *pPlayer, bool bIgnorePlayers, PlayerTeamSpawnMode_t nSpawnMode /* = 0*/ )
+{
+ bool bMatchSummary = ShowMatchSummary();
+
+ // Check the team.
+ // In Item Testing mode, bots all use the Red team spawns, and the player uses Blue
+ if ( IsInItemTestingMode() )
+ {
+ if ( pSpot->GetTeamNumber() != (pPlayer->IsFakeClient() ? TF_TEAM_RED : TF_TEAM_BLUE) )
+ return false;
+ }
+ else
+ {
+ if ( !bMatchSummary )
+ {
+ if ( pSpot->GetTeamNumber() != pPlayer->GetTeamNumber() )
+ return false;
+ }
+ }
+
+ if ( !pSpot->IsTriggered( pPlayer ) )
+ return false;
+
+ CTFTeamSpawn *pCTFSpawn = dynamic_cast<CTFTeamSpawn*>( pSpot );
+ if ( pCTFSpawn )
+ {
+ if ( pCTFSpawn->IsDisabled() )
+ return false;
+
+ if ( pCTFSpawn->GetTeamSpawnMode() && pCTFSpawn->GetTeamSpawnMode() != nSpawnMode )
+ return false;
+
+ if ( bMatchSummary )
+ {
+ if ( pCTFSpawn->AlreadyUsedForMatchSummary() )
+ return false;
+
+ if ( pCTFSpawn->GetMatchSummaryType() == PlayerTeamSpawn_MatchSummary_Winner )
+ {
+ if ( pPlayer->GetTeamNumber() != GetWinningTeam() )
+ return false;
+ }
+ else if ( pCTFSpawn->GetMatchSummaryType() == PlayerTeamSpawn_MatchSummary_Loser )
+ {
+ if ( pPlayer->GetTeamNumber() == GetWinningTeam() )
+ return false;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if ( pCTFSpawn->GetMatchSummaryType() != PlayerTeamSpawn_MatchSummary_None )
+ return false;
+ }
+ }
+
+ Vector mins = VEC_HULL_MIN_SCALED( pPlayer );
+ Vector maxs = VEC_HULL_MAX_SCALED( pPlayer );
+
+ if ( !bIgnorePlayers && !bMatchSummary )
+ {
+ Vector vTestMins = pSpot->GetAbsOrigin() + mins;
+ Vector vTestMaxs = pSpot->GetAbsOrigin() + maxs;
+ return UTIL_IsSpaceEmpty( pPlayer, vTestMins, vTestMaxs );
+ }
+
+ trace_t trace;
+ UTIL_TraceHull( pSpot->GetAbsOrigin(), pSpot->GetAbsOrigin(), mins, maxs, MASK_PLAYERSOLID, pPlayer, COLLISION_GROUP_PLAYER_MOVEMENT, &trace );
+ if ( trace.fraction == 1 && trace.allsolid != 1 && (trace.startsolid != 1) )
+ {
+ if ( bMatchSummary )
+ {
+ if ( pCTFSpawn )
+ {
+ pCTFSpawn->SetAlreadyUsedForMatchSummary();
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+Vector CTFGameRules::VecItemRespawnSpot( CItem *pItem )
+{
+ return pItem->GetOriginalSpawnOrigin();
+}
+
+QAngle CTFGameRules::VecItemRespawnAngles( CItem *pItem )
+{
+ return pItem->GetOriginalSpawnAngles();
+}
+
+int CTFGameRules::ItemShouldRespawn( CItem *pItem )
+{
+ return BaseClass::ItemShouldRespawn( pItem );
+}
+
+float CTFGameRules::FlItemRespawnTime( CItem *pItem )
+{
+ return ITEM_RESPAWN_TIME;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+const char *CTFGameRules::GetChatFormat( bool bTeamOnly, CBasePlayer *pPlayer )
+{
+ if ( !pPlayer ) // dedicated server output
+ {
+ return NULL;
+ }
+
+ const char *pszFormat = NULL;
+
+ // coach?
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+ if ( pTFPlayer && pTFPlayer->IsCoaching() )
+ {
+ pszFormat = "TF_Chat_Coach";
+ }
+ // team only
+ else if ( bTeamOnly == true )
+ {
+ if ( pPlayer->GetTeamNumber() == TEAM_SPECTATOR )
+ {
+ pszFormat = "TF_Chat_Spec";
+ }
+ else
+ {
+ if ( pPlayer->IsAlive() == false && State_Get() != GR_STATE_TEAM_WIN )
+ {
+ pszFormat = "TF_Chat_Team_Dead";
+ }
+ else
+ {
+ const char *chatLocation = GetChatLocation( bTeamOnly, pPlayer );
+ if ( chatLocation && *chatLocation )
+ {
+ pszFormat = "TF_Chat_Team_Loc";
+ }
+ else
+ {
+ pszFormat = "TF_Chat_Team";
+ }
+ }
+ }
+ }
+ // everyone
+ else
+ {
+ if ( pPlayer->GetTeamNumber() == TEAM_SPECTATOR )
+ {
+ pszFormat = "TF_Chat_AllSpec";
+ }
+ else
+ {
+ if ( pPlayer->IsAlive() == false && State_Get() != GR_STATE_TEAM_WIN )
+ {
+ pszFormat = "TF_Chat_AllDead";
+ }
+ else
+ {
+ pszFormat = "TF_Chat_All";
+ }
+ }
+ }
+
+ return pszFormat;
+}
+
+VoiceCommandMenuItem_t *CTFGameRules::VoiceCommand( CBaseMultiplayerPlayer *pPlayer, int iMenu, int iItem )
+{
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+ if ( pTFPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) )
+ {
+ engine->ClientCommand( pTFPlayer->edict(), "boo" );
+ return NULL;
+ }
+
+ VoiceCommandMenuItem_t *pItem = BaseClass::VoiceCommand( pPlayer, iMenu, iItem );
+
+ if ( pItem )
+ {
+ int iActivity = ActivityList_IndexForName( pItem->m_szGestureActivity );
+
+ if ( iActivity != ACT_INVALID )
+ {
+ if ( pTFPlayer )
+ {
+ pTFPlayer->DoAnimationEvent( PLAYERANIMEVENT_VOICE_COMMAND_GESTURE, iActivity );
+
+ // Note when we call for a Medic.
+ // Hardcoding this for menu 0, item 0 is an ugly hack, but we don't have a good way to
+ // translate this at this level without plumbing a through bunch of stuff (MSB)
+ if ( iMenu == 0 && iItem == 0 )
+ {
+ pTFPlayer->NoteMedicCall();
+ }
+ }
+ }
+ }
+
+ return pItem;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Actually change a player's name.
+//-----------------------------------------------------------------------------
+void CTFGameRules::ChangePlayerName( CTFPlayer *pPlayer, const char *pszNewName )
+{
+ const char *pszOldName = pPlayer->GetPlayerName();
+
+ // Check if they can change their name
+ // don't show when bots change their names in PvE mode
+ if ( State_Get() != GR_STATE_STALEMATE && !( IsPVEModeActive() && pPlayer->IsBot() ) )
+ {
+ CReliableBroadcastRecipientFilter filter;
+ UTIL_SayText2Filter( filter, pPlayer, false, "#TF_Name_Change", pszOldName, pszNewName );
+
+ IGameEvent * event = gameeventmanager->CreateEvent( "player_changename" );
+ if ( event )
+ {
+ event->SetInt( "userid", pPlayer->GetUserID() );
+ event->SetString( "oldname", pszOldName );
+ event->SetString( "newname", pszNewName );
+ gameeventmanager->FireEvent( event );
+ }
+ }
+
+ pPlayer->SetPlayerName( pszNewName );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::ClientSettingsChanged( CBasePlayer *pPlayer )
+{
+ const char *pszName = engine->GetClientConVarValue( pPlayer->entindex(), "name" );
+
+ const char *pszOldName = pPlayer->GetPlayerName();
+
+ CTFPlayer *pTFPlayer = (CTFPlayer*)pPlayer;
+
+ // msg everyone if someone changes their name, and it isn't the first time (changing no name to current name)
+ // Note, not using FStrEq so that this is case sensitive
+ if ( pszOldName[0] != 0 && Q_strncmp( pszOldName, pszName, MAX_PLAYER_NAME_LENGTH-1 ) )
+ {
+ ChangePlayerName( pTFPlayer, pszName );
+ }
+
+ // keep track of their hud_classautokill value
+ int nClassAutoKill = Q_atoi( engine->GetClientConVarValue( pPlayer->entindex(), "hud_classautokill" ) );
+ pTFPlayer->SetHudClassAutoKill( nClassAutoKill > 0 ? true : false );
+
+ // keep track of their tf_medigun_autoheal value
+ pTFPlayer->SetMedigunAutoHeal( Q_atoi( engine->GetClientConVarValue( pPlayer->entindex(), "tf_medigun_autoheal" ) ) > 0 );
+
+ // keep track of their cl_autorezoom value
+ pTFPlayer->SetAutoRezoom( Q_atoi( engine->GetClientConVarValue( pPlayer->entindex(), "cl_autorezoom" ) ) > 0 );
+ pTFPlayer->SetAutoReload( Q_atoi( engine->GetClientConVarValue( pPlayer->entindex(), "cl_autoreload" ) ) > 0 );
+
+ // keep track of their tf_remember_lastswitched value
+ pTFPlayer->SetRememberActiveWeapon( Q_atoi( engine->GetClientConVarValue( pPlayer->entindex(), "tf_remember_activeweapon" ) ) > 0 );
+ pTFPlayer->SetRememberLastWeapon( Q_atoi( engine->GetClientConVarValue( pPlayer->entindex(), "tf_remember_lastswitched" ) ) > 0 );
+
+ const char *pszFov = engine->GetClientConVarValue( pPlayer->entindex(), "fov_desired" );
+ int iFov = atoi(pszFov);
+ iFov = clamp( iFov, 75, MAX_FOV );
+
+ pTFPlayer->SetDefaultFOV( iFov );
+
+ pTFPlayer->m_bFlipViewModels = Q_strcmp( engine->GetClientConVarValue( pPlayer->entindex(), "cl_flipviewmodels" ), "1" ) == 0;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Return true if the specified player can carry any more of the ammo type
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CanHaveAmmo( CBaseCombatCharacter *pPlayer, int iAmmoIndex )
+{
+ if ( iAmmoIndex > -1 )
+ {
+ CTFPlayer *pTFPlayer = (CTFPlayer*)pPlayer;
+
+ if ( pTFPlayer )
+ {
+ // Get the max carrying capacity for this ammo
+ int iMaxCarry = pTFPlayer->GetMaxAmmo(iAmmoIndex);
+
+ // Does the player have room for more of this type of ammo?
+ if ( pTFPlayer->GetAmmoCount( iAmmoIndex ) < iMaxCarry )
+ return true;
+ }
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool EconEntity_KillEaterEventPassesRestrictionCheck( const IEconItemInterface *pEconInterface, const CEconItemAttributeDefinition *pRestrictionAttribDef, const CEconItemAttributeDefinition *pRestrictionValueAttribDef, const CSteamID VictimSteamID )
+{
+ uint32 unRestrictionType = kStrangeEventRestriction_None;
+ attrib_value_t unRestrictionValue;
+ DbgVerify( pEconInterface->FindAttribute( pRestrictionAttribDef, &unRestrictionType ) ==
+ pEconInterface->FindAttribute( pRestrictionValueAttribDef, &unRestrictionValue ) );
+
+ switch (unRestrictionType)
+ {
+ case kStrangeEventRestriction_None:
+ return true;
+
+ case kStrangeEventRestriction_Map:
+ {
+ const MapDef_t *pMap = GetItemSchema()->GetMasterMapDefByName(STRING(gpGlobals->mapname));
+ return pMap && pMap->m_nDefIndex == unRestrictionValue;
+ }
+ case kStrangeEventRestriction_VictimSteamAccount:
+ return VictimSteamID.GetAccountID() == unRestrictionValue;
+ case kStrangeEventRestriction_Competitive:
+ {
+ if ( TFGameRules() && TFGameRules()->IsMatchTypeCompetitive() )
+ {
+ // Match Season unRestrictionValue == Season Number
+ return true;
+ }
+ return false;
+ }
+ } // end Switch
+
+ AssertMsg1( false, "Unhandled strange event restriction %u!", unRestrictionType );
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+static CSteamID GetSteamIDForKillEaterScoring( CTFPlayer *pPlayer )
+{
+ if ( pPlayer->IsBot() )
+ return k_steamIDNil;
+
+ CSteamID ret;
+ if ( !pPlayer->GetSteamID( &ret ) )
+ return k_steamIDNil;
+
+ return ret;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+class CStrangeEventValidator
+{
+public:
+ CStrangeEventValidator( IEconItemInterface *pEconEntity, CTFPlayer *pOwner, CTFPlayer *pVictim, kill_eater_event_t eEventType )
+ : m_unItemID( INVALID_ITEM_ID )
+ , m_eEventType( eEventType )
+ {
+ m_bIsValid = BInitEventParams( pEconEntity, pOwner, pVictim );
+ }
+
+ bool BIsValidEvent() const { return m_bIsValid; }
+ CSteamID GetKillerSteamID() const { Assert( m_bIsValid ); return m_KillerSteamID; }
+ CSteamID GetVictimSteamID() const { Assert( m_bIsValid ); return m_VictimSteamID; }
+ itemid_t GetItemID() const { Assert( m_bIsValid ); return m_unItemID; }
+ kill_eater_event_t GetEventType() const { Assert( m_bIsValid ); return m_eEventType; }
+
+private:
+ bool BInitEventParams( IEconItemInterface *pEconEntity, CTFPlayer *pOwner, CTFPlayer *pVictim )
+ {
+ static CSchemaAttributeDefHandle pAttrDef_KillEater( "kill eater" );
+
+ // Kill-eater weapons.
+ if ( !pEconEntity )
+ return false;
+
+ if ( !pOwner )
+ return false;
+
+ if ( !pVictim )
+ return false;
+
+ // Ignore events where we're affecting ourself.
+ if ( pOwner == pVictim )
+ return false;
+
+ // Store off our item ID for reference post-init.
+ m_unItemID = pEconEntity->GetID();
+
+ // Always require that we have at least the base kill eater attribute before sending any messages
+ // to the GC.
+ if ( !pEconEntity->FindAttribute( pAttrDef_KillEater ) )
+ return false;
+
+ // Don't bother sending a message to the GC if either party is a bot, unless we're tracking events against
+ // bots specifically.
+ if ( !pOwner->GetSteamID( &m_KillerSteamID ) )
+ return false;
+
+ m_VictimSteamID = GetSteamIDForKillEaterScoring( pVictim );
+ if ( !m_VictimSteamID.IsValid() )
+ {
+ if ( !GetItemSchema()->GetKillEaterScoreTypeAllowsBotVictims( m_eEventType ) )
+ return false;
+ }
+
+ // Also require that we have whatever event type we're looking for, unless we're looking for regular
+ // player kills in which case we may or may not have a field to describe that.
+ for ( int i = 0; i < GetKillEaterAttrCount(); i++ )
+ {
+ const CEconItemAttributeDefinition *pScoreAttribDef = GetKillEaterAttr_Score(i);
+ if ( !pScoreAttribDef )
+ continue;
+
+ // If we don't have this attribute, move on. It's possible to be missing this attribute but still
+ // have the next one in the list if we have user-customized tracking types.
+ if ( !pEconEntity->FindAttribute( pScoreAttribDef ) )
+ continue;
+
+ const CEconItemAttributeDefinition *pScoreTypeAttribDef = GetKillEaterAttr_Type(i);
+ if ( !pScoreTypeAttribDef )
+ continue;
+
+ // If we're missing our type attribute, we interpret that as "track kills" -- none of the original
+ // kill eaters have score type attributes.
+ float fKillEaterEventType;
+ kill_eater_event_t eMatchEvent = FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pEconEntity, pScoreTypeAttribDef, &fKillEaterEventType )
+ ? (kill_eater_event_t)(int)fKillEaterEventType
+ : kKillEaterEvent_PlayerKill;
+
+ if ( m_eEventType == eMatchEvent )
+ {
+ // Does this attribute also specify a restriction (ie., "has to be on this specific map" or "has to be
+ // against this specific account"? If so, only count this as a match if the restriction check passes.
+ if ( EconEntity_KillEaterEventPassesRestrictionCheck( pEconEntity, GetKillEaterAttr_Restriction(i), GetKillEaterAttr_RestrictionValue(i), m_VictimSteamID ) )
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+private:
+ bool m_bIsValid;
+ CSteamID m_KillerSteamID;
+ CSteamID m_VictimSteamID;
+ itemid_t m_unItemID;
+ kill_eater_event_t m_eEventType;
+};
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+template < typename tMsgType >
+static void FillOutBaseKillEaterMessage( tMsgType *out_pMsg, const CStrangeEventValidator& Validator )
+{
+ Assert( out_pMsg );
+ Assert( Validator.BIsValidEvent() );
+
+ out_pMsg->set_killer_steam_id( Validator.GetKillerSteamID().ConvertToUint64() );
+ out_pMsg->set_victim_steam_id( Validator.GetVictimSteamID().ConvertToUint64() );
+ out_pMsg->set_item_id( Validator.GetItemID() );
+ out_pMsg->set_event_type( Validator.GetEventType() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Get the actual killeater weapon from a CTakeDamageInfo
+//-----------------------------------------------------------------------------
+CTFWeaponBase *GetKilleaterWeaponFromDamageInfo( const CTakeDamageInfo *pInfo )
+{
+ CTFWeaponBase *pTFWeapon = dynamic_cast<CTFWeaponBase *>( pInfo->GetWeapon() );
+ CBaseEntity* pInflictor = pInfo->GetInflictor();
+
+ // If there's no weapon specified, it might have been from a sentry
+ if ( !pTFWeapon )
+ {
+ // If a sentry did the damage with bullets, it's the inflictor
+ CObjectSentrygun *pSentrygun = dynamic_cast<CObjectSentrygun *>( pInflictor );
+ if ( !pSentrygun )
+ {
+ // If a sentry's rocket did the damage, then the rocket is the inflictor, and the sentry is the rocket's owner
+ pSentrygun = pInflictor ? dynamic_cast<CObjectSentrygun *>( pInflictor->GetOwnerEntity() ) : NULL;
+ }
+
+ if ( pSentrygun )
+ {
+ // Maybe they were using a Wrangler? The builder is the player who owns the sentry
+ CTFPlayer *pBuilder = pSentrygun->GetBuilder();
+ if ( pBuilder )
+ {
+ // Check if they have a Wrangler and were aiming with it
+ CTFLaserPointer* pLaserPointer = dynamic_cast< CTFLaserPointer * >( pBuilder->GetEntityForLoadoutSlot( LOADOUT_POSITION_SECONDARY ) );
+ if ( pLaserPointer && pLaserPointer->HasLaserDot() )
+ {
+ pTFWeapon = pLaserPointer;
+ }
+ }
+ }
+ }
+
+ // need to check for deflected projectiles so the weapon that did the most recent
+ // deflection will get the killeater credit instead of the original launcher
+ if ( pInflictor )
+ {
+ CTFWeaponBaseGrenadeProj *pBaseGrenade = dynamic_cast< CTFWeaponBaseGrenadeProj* >( pInflictor );
+ if ( pBaseGrenade )
+ {
+ if ( pBaseGrenade->GetDeflected() && pBaseGrenade->GetLauncher() )
+ {
+ pTFWeapon = dynamic_cast< CTFWeaponBase* >( pBaseGrenade->GetLauncher() );
+ }
+ }
+ else
+ {
+ CTFBaseRocket *pRocket = dynamic_cast< CTFBaseRocket* >( pInflictor );
+ if ( pRocket )
+ {
+ if ( pRocket->GetDeflected() && pRocket->GetLauncher() )
+ {
+ pTFWeapon = dynamic_cast< CTFWeaponBase* >( pRocket->GetLauncher() );
+ }
+ }
+ }
+ }
+
+ return pTFWeapon;
+}
+
+//-----------------------------------------------------------------------------
+void EconEntity_ValidateAndSendStrangeMessageToGC( IEconItemInterface *pEconEntity, CTFPlayer *pOwner, CTFPlayer *pVictim, kill_eater_event_t eEventType, int nIncrementValue /*= 1*/ )
+{
+ CStrangeEventValidator Validator( pEconEntity, pOwner, pVictim, eEventType );
+
+ if ( !Validator.BIsValidEvent() )
+ return;
+
+ GCSDK::CProtoBufMsg<CMsgIncrementKillCountAttribute> msg( k_EMsgGC_IncrementKillCountAttribute );
+ FillOutBaseKillEaterMessage( &msg.Body(), Validator );
+
+ if ( nIncrementValue != 1 )
+ {
+ msg.Body().set_increment_value( nIncrementValue );
+ }
+
+ GCClientSystem()->BSendMessage( msg );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void EconEntity_OnOwnerKillEaterEvent( CEconEntity *pEconEntity, CTFPlayer *pOwner, CTFPlayer *pVictim, kill_eater_event_t eEventType, int nIncrementValue /*= 1*/ )
+{
+ if ( !pEconEntity )
+ return;
+
+ EconItemInterface_OnOwnerKillEaterEvent( pEconEntity->GetAttributeContainer()->GetItem(), pOwner, pVictim, eEventType, nIncrementValue );
+}
+//-----------------------------------------------------------------------------
+void EconItemInterface_OnOwnerKillEaterEvent( IEconItemInterface *pEconEntity, CTFPlayer *pOwner, CTFPlayer *pVictim, kill_eater_event_t eEventType, int nIncrementValue /*= 1*/ )
+{
+ bool bEconEntityHandled = false;
+
+ // Fire the kill eater event on all wearables
+ // Wearables can have all Strange parts
+ for ( int i = 0; i < pOwner->GetNumWearables(); ++i )
+ {
+ CTFWearable *pWearableItem = dynamic_cast<CTFWearable *>( pOwner->GetWearable( i ) );
+ if ( !pWearableItem )
+ continue;
+
+ if ( !pWearableItem->GetAttributeContainer() )
+ continue;
+
+ EconEntity_ValidateAndSendStrangeMessageToGC( pWearableItem->GetAttributeContainer()->GetItem(), pOwner, pVictim, eEventType, nIncrementValue );
+
+ if ( pWearableItem->GetAttributeContainer()->GetItem() == pEconEntity )
+ {
+ bEconEntityHandled = true;
+ }
+ }
+
+ if ( !bEconEntityHandled )
+ {
+ EconEntity_ValidateAndSendStrangeMessageToGC( pEconEntity, pOwner, pVictim, eEventType, nIncrementValue );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void EconEntity_OnOwnerKillEaterEventNoPartner( CEconEntity *pEconEntity, CTFPlayer *pOwner, kill_eater_event_t eEventType, int nIncrementValue /*= 1*/ )
+{
+ if ( !pEconEntity )
+ return;
+
+ EconItemInterface_OnOwnerKillEaterEventNoPartner( pEconEntity->GetAttributeContainer()->GetItem(), pOwner, eEventType, nIncrementValue );
+}
+//-----------------------------------------------------------------------------
+void EconItemInterface_OnOwnerKillEaterEventNoPartner( IEconItemInterface *pEconEntity, CTFPlayer *pOwner, kill_eater_event_t eEventType, int nIncrementValue /*= 1*/ )
+{
+ // Look for another player to pass in. We do two passes over the list of candidates.
+ // First we look for anyone that we know is a real player. If we don't find any of
+ // those then we pass in a known bot. This means that things that allow bots will
+ // still find one and work, but event types that don't allow bots won't be dependent
+ // on the order of players in the list.
+ for( int iPass = 1; iPass <= 2; iPass++ )
+ {
+ for( int playerIndex = 1; playerIndex <= MAX_PLAYERS; playerIndex++ )
+ {
+ CTFPlayer *pOtherPlayer = ToTFPlayer( UTIL_PlayerByIndex( playerIndex ) );
+ if ( !pOtherPlayer )
+ continue;
+
+ // Ignore ourself.
+ if ( pOtherPlayer == pOwner )
+ continue;
+
+
+ const bool bCandidatePasses = (iPass == 1 && GetSteamIDForKillEaterScoring( pOtherPlayer ).IsValid()) // if we have a real Steam ID we're golden
+ || (iPass == 2); // or if we made it to the second pass, any valid player pointer is as good as any other
+ if ( bCandidatePasses )
+ {
+ EconItemInterface_OnOwnerKillEaterEvent( pEconEntity, pOwner, pOtherPlayer, eEventType, nIncrementValue );
+ return;
+ }
+ }
+ }
+
+ // We couldn't find anyone else at all. This is hard. Let's give up.
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+static GCSDK::CProtoBufMsg<CMsgIncrementKillCountAttribute_Multiple> *s_pmsgIncrementKillCountMessageBatch = NULL;
+//-----------------------------------------------------------------------------
+void EconEntity_ValidateAndSendStrangeMessageToGC_Batched( IEconItemInterface *pEconInterface, class CTFPlayer *pOwner, class CTFPlayer *pVictim, kill_eater_event_t eEventType, int nIncrementValue /*= 1*/ )
+{
+ CStrangeEventValidator Validator( pEconInterface, pOwner, pVictim, eEventType );
+
+ if ( !Validator.BIsValidEvent() )
+ return;
+
+ // This has to be dynamically allocated because we can't static-init a protobuf message at startup type.
+ if ( !s_pmsgIncrementKillCountMessageBatch )
+ {
+ s_pmsgIncrementKillCountMessageBatch = new GCSDK::CProtoBufMsg<CMsgIncrementKillCountAttribute_Multiple>( k_EMsgGC_IncrementKillCountAttribute_Multiple );
+ }
+
+ // Look for an existing message that matches this user and this event.
+ for ( int i = 0; i < s_pmsgIncrementKillCountMessageBatch->Body().msgs_size(); i++ )
+ {
+ CMsgIncrementKillCountAttribute *pMsg = s_pmsgIncrementKillCountMessageBatch->Body().mutable_msgs( i );
+ Assert( pMsg );
+
+ if ( pMsg->killer_steam_id() == Validator.GetKillerSteamID().ConvertToUint64() &&
+ pMsg->victim_steam_id() == Validator.GetVictimSteamID().ConvertToUint64() &&
+ pMsg->item_id() == Validator.GetItemID() &&
+ (kill_eater_event_t)pMsg->event_type() == Validator.GetEventType() )
+ {
+ // We found an existing entry for this message. All we want to do is add to the stat we're tracking
+ // but still send up this single message.
+ Assert( pMsg->has_increment_value() );
+
+ pMsg->set_increment_value( pMsg->increment_value() + nIncrementValue );
+ return;
+ }
+ }
+
+ // We don't have an existing entry, so make a new one.
+ CMsgIncrementKillCountAttribute *pNewMsg = s_pmsgIncrementKillCountMessageBatch->Body().add_msgs();
+
+ FillOutBaseKillEaterMessage( pNewMsg, Validator );
+ pNewMsg->set_increment_value( nIncrementValue );
+}
+//-----------------------------------------------------------------------------
+void EconEntity_OnOwnerKillEaterEvent_Batched( CEconEntity *pEconEntity, class CTFPlayer *pOwner, class CTFPlayer *pVictim, kill_eater_event_t eEventType, int nIncrementValue /*= 1*/ )
+{
+ if ( !pEconEntity )
+ return;
+
+ EconItemInterface_OnOwnerKillEaterEvent_Batched( pEconEntity->GetAttributeContainer()->GetItem(), pOwner, pVictim, eEventType, nIncrementValue );
+}
+//-----------------------------------------------------------------------------
+void EconItemInterface_OnOwnerKillEaterEvent_Batched( IEconItemInterface *pEconInterface, class CTFPlayer *pOwner, class CTFPlayer *pVictim, kill_eater_event_t eEventType, int nIncrementValue /*= 1*/ )
+{
+ for ( int i = 0; i < pOwner->GetNumWearables(); ++i )
+ {
+ CTFWearable *pWearableItem = dynamic_cast<CTFWearable *>( pOwner->GetWearable( i ) );
+ if ( !pWearableItem )
+ continue;
+
+ if ( !pWearableItem->GetAttributeContainer() )
+ continue;
+
+ CEconItemView *pEconItemView = pWearableItem->GetAttributeContainer()->GetItem();
+ if ( !pEconItemView )
+ continue;
+
+ EconEntity_ValidateAndSendStrangeMessageToGC_Batched( pWearableItem->GetAttributeContainer()->GetItem(), pOwner, pVictim, eEventType, nIncrementValue );
+ }
+
+ EconEntity_ValidateAndSendStrangeMessageToGC_Batched( pEconInterface, pOwner, pVictim, eEventType, nIncrementValue );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void KillEaterEvents_FlushBatches()
+{
+ if ( s_pmsgIncrementKillCountMessageBatch )
+ {
+ Assert( s_pmsgIncrementKillCountMessageBatch->Body().msgs_size() > 0 );
+
+ GCClientSystem()->BSendMessage( *s_pmsgIncrementKillCountMessageBatch );
+
+ delete s_pmsgIncrementKillCountMessageBatch;
+ s_pmsgIncrementKillCountMessageBatch = NULL;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void HatAndMiscEconEntities_OnOwnerKillEaterEventNoParter( class CTFPlayer *pOwner, kill_eater_event_t eEventType, int nIncrementValue )
+{
+ EconItemInterface_OnOwnerKillEaterEventNoPartner( NULL, pOwner, eEventType, nIncrementValue );
+}
+
+//-----------------------------------------------------------------------------
+void HatAndMiscEconEntities_OnOwnerKillEaterEvent( class CTFPlayer *pOwner, class CTFPlayer *pVictim, kill_eater_event_t eEventType, int nIncrementValue )
+{
+ EconItemInterface_OnOwnerKillEaterEvent( NULL, pOwner, pVictim, eEventType, nIncrementValue );
+}
+
+//-----------------------------------------------------------------------------
+void EconEntity_OnOwnerUniqueEconEvent( IEconItemInterface *pEconEntity, CTFPlayer *pOwner, CTFPlayer *pVictim, kill_eater_event_t eEventType )
+{
+ CStrangeEventValidator Validator( pEconEntity, pOwner, pVictim, eEventType );
+
+ if ( !Validator.BIsValidEvent() )
+ return;
+
+ GCSDK::CProtoBufMsg<CMsgTrackUniquePlayerPairEvent> msg( k_EMsgGC_TrackUniquePlayerPairEvent );
+ FillOutBaseKillEaterMessage( &msg.Body(), Validator );
+
+ GCClientSystem()->BSendMessage( msg );
+}
+
+//-----------------------------------------------------------------------------
+void EconEntity_OnOwnerUniqueEconEvent( CEconEntity *pEconEntity, CTFPlayer *pOwner, CTFPlayer *pVictim, kill_eater_event_t eEventType )
+{
+ if ( !pEconEntity )
+ return;
+ EconEntity_OnOwnerUniqueEconEvent( pEconEntity->GetAttributeContainer()->GetItem(), pOwner, pVictim, eEventType );
+}
+
+//-----------------------------------------------------------------------------
+void EconEntity_NonEquippedItemKillTracking( CTFPlayer *pOwner, CTFPlayer *pVictim, item_definition_index_t iDefIndex, kill_eater_event_t eEventType, int nIncrementValue = 1 )
+{
+ // Validate
+ if ( !pOwner || !pVictim || pOwner == pVictim )
+ return;
+
+#ifndef STAGING_ONLY
+ if ( pOwner->IsBot() || pVictim->IsBot() )
+ return;
+#endif
+
+ CSteamID pOwnerSteamID;
+ CSteamID pVictimSteamID;
+ if ( !pOwner->GetSteamID( &pOwnerSteamID ) || !pVictim->GetSteamID( &pVictimSteamID ) )
+ return;
+
+ // Current date
+ static CSchemaAttributeDefHandle pAttrDef_DeactiveDate( "deactive date" );
+ uint32 unCurrentDate = CRTime::RTime32TimeCur();
+
+ CEconItemView *pNonEquippedItem = pOwner->Inventory()->GetFirstItemOfItemDef( iDefIndex, pOwner->Inventory() );
+ if ( pNonEquippedItem )
+ {
+ // Check the date
+ uint32 unDeactiveDate = 0;
+ if ( !pNonEquippedItem->FindAttribute( pAttrDef_DeactiveDate, &unDeactiveDate ) || unDeactiveDate > unCurrentDate )
+ {
+ GCSDK::CProtoBufMsg<CMsgIncrementKillCountAttribute> msg( k_EMsgGC_IncrementKillCountAttribute );
+ msg.Body().set_killer_steam_id( pOwnerSteamID.ConvertToUint64() );
+ msg.Body().set_victim_steam_id( pVictimSteamID.ConvertToUint64() );
+
+ if ( nIncrementValue > 1 )
+ {
+ msg.Body().set_increment_value( nIncrementValue );
+ }
+
+ msg.Body().set_item_id( pNonEquippedItem->GetID() );
+ msg.Body().set_event_type( eEventType );
+ GCClientSystem()->BSendMessage( msg );
+ }
+ }
+
+}
+
+//-----------------------------------------------------------------------------
+void EconEntity_NonEquippedItemKillTracking_NoPartner( CTFPlayer *pOwner, item_definition_index_t iDefIndex, kill_eater_event_t eEventType, int nIncrementValue )
+{
+ // Find a partner or give up
+ for( int iPass = 1; iPass <= 2; iPass++ )
+ {
+ for( int playerIndex = 1; playerIndex <= MAX_PLAYERS; playerIndex++ )
+ {
+ CTFPlayer *pOtherPlayer = ToTFPlayer( UTIL_PlayerByIndex( playerIndex ) );
+ if ( !pOtherPlayer )
+ continue;
+
+ // Ignore ourself.
+ if ( pOtherPlayer == pOwner )
+ continue;
+
+
+ const bool bCandidatePasses = (iPass == 1 && GetSteamIDForKillEaterScoring( pOtherPlayer ).IsValid()) // if we have a real Steam ID we're golden
+ || (iPass == 2); // or if we made it to the second pass, any valid player pointer is as good as any other
+
+ if ( bCandidatePasses )
+ {
+ EconEntity_NonEquippedItemKillTracking( pOwner, pOtherPlayer, iDefIndex, eEventType, nIncrementValue );
+ return;
+ }
+ }
+ }
+}
+
+void EconEntity_NonEquippedItemKillTracking_NoPartnerBatched( class CTFPlayer *pOwner, item_definition_index_t iDefIndex, kill_eater_event_t eEventType, int nIncrementValue )
+{
+ // Find a partner or give up
+ for ( int iPass = 1; iPass <= 2; iPass++ )
+ {
+ for ( int playerIndex = 1; playerIndex <= MAX_PLAYERS; playerIndex++ )
+ {
+ CTFPlayer *pOtherPlayer = ToTFPlayer( UTIL_PlayerByIndex( playerIndex ) );
+ if ( !pOtherPlayer )
+ continue;
+
+ // Ignore ourself.
+ if ( pOtherPlayer == pOwner )
+ continue;
+
+
+ const bool bCandidatePasses = ( iPass == 1 && GetSteamIDForKillEaterScoring( pOtherPlayer ).IsValid() ) // if we have a real Steam ID we're golden
+ || ( iPass == 2 ); // or if we made it to the second pass, any valid player pointer is as good as any other
+
+ if ( bCandidatePasses )
+ {
+ // find the item
+ // Current date
+ static CSchemaAttributeDefHandle pAttrDef_DeactiveDate( "deactive date" );
+ uint32 unCurrentDate = CRTime::RTime32TimeCur();
+
+ CEconItemView *pNonEquippedItem = pOwner->Inventory()->GetFirstItemOfItemDef( iDefIndex, pOwner->Inventory() );
+ if ( pNonEquippedItem )
+ {
+ uint32 unDeactiveDate = 0;
+ if ( !pNonEquippedItem->FindAttribute( pAttrDef_DeactiveDate, &unDeactiveDate ) || unDeactiveDate > unCurrentDate )
+ {
+ EconEntity_ValidateAndSendStrangeMessageToGC_Batched( pNonEquippedItem, pOwner, pOtherPlayer, eEventType, nIncrementValue );
+ }
+ }
+
+ return;
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// TODO: Remove this call and only use the one above
+//-----------------------------------------------------------------------------
+void EconEntity_NonEquippedItemKillTracking( CTFPlayer *pOwner, CTFPlayer *pVictim, int nIncrementValue )
+{
+ // Validate
+ if ( !pOwner || !pVictim || pOwner == pVictim )
+ return;
+
+ if ( pOwner->IsBot() || pVictim->IsBot() )
+ return;
+
+ CSteamID pOwnerSteamID;
+ CSteamID pVictimSteamID;
+ if ( ! pOwner->GetSteamID( &pOwnerSteamID ) || !pVictim->GetSteamID( &pVictimSteamID ) )
+ return;
+
+ // Find any active operation
+ const auto& mapOperations = GetItemSchema()->GetOperationDefinitions();
+ FOR_EACH_MAP_FAST( mapOperations, i )
+ {
+ CEconOperationDefinition* pOperation = mapOperations[i];
+ if ( pOperation->IsActive() && pOperation->IsCampaign() )
+ {
+ EconEntity_NonEquippedItemKillTracking( pOwner, pVictim, pOperation->GetRequiredItemDefIndex(), kKillEaterEvent_CosmeticOperationKills, nIncrementValue );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Sends a soul with the specified value to every living member of the
+// specified team, originating from vecPosition.
+//-----------------------------------------------------------------------------
+void CTFGameRules::DropHalloweenSoulPackToTeam( int nAmount, const Vector& vecPosition, int nTeamNumber, int nSourceTeam )
+{
+ for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+ if ( !pTFPlayer || !pTFPlayer->IsConnected() )
+ continue;
+
+ if ( pTFPlayer->GetTeamNumber() != nTeamNumber || !pTFPlayer->IsAlive() || pTFPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) )
+ continue;
+
+ DropHalloweenSoulPack( nAmount, vecPosition, pTFPlayer, nSourceTeam );
+ }
+}
+
+
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::DropHalloweenSoulPack( int nAmount, const Vector& vecSource, CBaseEntity *pTarget, int nSourceTeam )
+{
+ QAngle angles(0,0,0);
+ CHalloweenSoulPack *pSoulsPack = assert_cast<CHalloweenSoulPack*>( CBaseEntity::CreateNoSpawn( "halloween_souls_pack", vecSource, angles, NULL ) );
+
+ if ( pSoulsPack )
+ {
+ pSoulsPack->SetTarget( pTarget );
+ pSoulsPack->SetAmount( nAmount );
+ pSoulsPack->ChangeTeam( nSourceTeam );
+
+ DispatchSpawn( pSoulsPack );
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldDropSpellPickup()
+{
+ if ( IsUsingSpells() )
+ {
+ return RandomFloat() <= tf_player_spell_drop_on_death_rate.GetFloat();
+ }
+ return false;
+}
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::DropSpellPickup( const Vector& vPosition, int nTier /*= 0*/ ) const
+{
+ if ( !IsUsingSpells() )
+ return;
+
+ CSpellPickup *pSpellPickup = assert_cast<CSpellPickup*>( CBaseEntity::CreateNoSpawn( "tf_spell_pickup", vPosition + Vector( 0, 0, 50 ), vec3_angle, NULL ) );
+ if ( pSpellPickup )
+ {
+ pSpellPickup->SetTier( nTier );
+
+ DispatchSpawn( pSpellPickup );
+
+ Vector vecImpulse = RandomVector( -0.5f, 0.5f );
+ vecImpulse.z = 1.f;
+ VectorNormalize( vecImpulse );
+
+ Vector vecVelocity = vecImpulse * 500.f;
+ pSpellPickup->DropSingleInstance( vecVelocity, NULL, 0 );
+ }
+}
+
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldDropBonusDuck( void )
+{
+ if ( tf_player_drop_bonus_ducks.GetInt() < 0 )
+ {
+ return IsHolidayActive( kHoliday_EOTL );
+ }
+ else if ( tf_player_drop_bonus_ducks.GetInt() > 0 )
+ {
+ return true;
+ }
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldDropBonusDuckFromPlayer( CTFPlayer *pTFScorer, CTFPlayer *pTFVictim )
+{
+ if ( !pTFScorer || !pTFVictim )
+ return false;
+
+#ifndef STAGING_ONLY
+ // Only drop if bot is not involved
+ if ( pTFScorer->IsBot() || pTFVictim->IsBot() || pTFScorer == pTFVictim )
+ return false;
+#endif
+
+ return ShouldDropBonusDuck();
+}
+
+//-----------------------------------------------------------------------------
+int CTFGameRules::GetDuckSkinForClass( int nTeam, int nClass ) const
+{
+ int nSkin = 0;
+ if ( nTeam >= FIRST_GAME_TEAM )
+ {
+ bool bRed = ( nTeam == TF_TEAM_RED );
+
+ switch ( nClass )
+ {
+ case TF_CLASS_SCOUT:
+ nSkin = bRed ? 3 : 12;
+ break;
+ case TF_CLASS_SNIPER:
+ nSkin = bRed ? 4 : 13;
+ break;
+ case TF_CLASS_SOLDIER:
+ nSkin = bRed ? 5 : 14;
+ break;
+ case TF_CLASS_DEMOMAN:
+ nSkin = bRed ? 6 : 15;
+ break;
+ case TF_CLASS_MEDIC:
+ nSkin = bRed ? 7 : 16;
+ break;
+ case TF_CLASS_HEAVYWEAPONS:
+ nSkin = bRed ? 8 : 17;
+ break;
+ case TF_CLASS_PYRO:
+ nSkin = bRed ? 9 : 18;
+ break;
+ case TF_CLASS_SPY:
+ nSkin = bRed ? 10 : 19;
+ break;
+ case TF_CLASS_ENGINEER:
+ nSkin = bRed ? 11 : 20;
+ break;
+ default:
+ nSkin = bRed ? 1 : 2;
+ break;
+ }
+ }
+
+ return nSkin;
+}
+
+//-----------------------------------------------------------------------------
+//
+#ifdef STAGING_ONLY
+ConVar tf_duck_droprate_min( "tf_duck_droprate_min", "-1", FCVAR_REPLICATED, "Set Minimum Number of Ducks to Spawn. No upgrade" );
+ConVar tf_duck_droprate_bias( "tf_duck_droprate_bias", "0.2", FCVAR_REPLICATED, "Set Bias on Duck Spawns. No upgrade" );
+ConVar tf_duck_power_override( "tf_duck_power_override", "0", FCVAR_REPLICATED, "Override everyone's duck power and use this value when > 0" );
+ConVar tf_duck_random_extra( "tf_duck_random_extra", "3.0f", FCVAR_REPLICATED, "Random Count of Ducks to be added used by bias function" );
+#endif
+ConVar tf_duck_edict_limit( "tf_duck_edict_limit", "1900", FCVAR_REPLICATED, "Maximum number of edicts allowed before spawning a duck" );
+ConVar tf_duck_edict_warning( "tf_duck_edict_warning", "1800", FCVAR_REPLICATED, "Maximum number of edicts allowed before slowing duck spawn rate" );
+void CTFGameRules::DropBonusDuck( const Vector& vPosition, CTFPlayer *pTFCreator /*=NULL*/, CTFPlayer *pAssister /*=NULL*/, CTFPlayer *pTFVictim /*=NULL*/, bool bCrit /*=false*/, bool bObjective /*=false*/) const
+{
+ if ( gEntList.NumberOfEdicts() > tf_duck_edict_limit.GetInt() )
+ {
+ Warning( "Warning: High level of Edicts, Not spawning Ducks \n" );
+ return;
+ }
+
+ static CSchemaAttributeDefHandle pAttr_DuckLevelBadge( "duck badge level" );
+
+ // Find Badge and increase number of ducks based on badge level (1 duck per 5 levels)
+ // Look through equipped items, if one is a duck badge use its level
+ int iDuckFlags = 0;
+ if ( pTFCreator == NULL || bObjective )
+ {
+ iDuckFlags |= EDuckFlags::DUCK_FLAG_OBJECTIVE;
+ }
+ uint32 iDuckBadgeLevel = 0;
+
+ if ( pTFCreator )
+ {
+ for ( int i = 0; i < pTFCreator->GetNumWearables(); ++i )
+ {
+ CTFWearable* pWearable = dynamic_cast<CTFWearable*>( pTFCreator->GetWearable( i ) );
+ if ( !pWearable )
+ continue;
+
+ //if ( pWearable->GetAttributeContainer() && pWearable->GetAttributeContainer()->GetItem() )
+ {
+ //CEconItemView *pItem = pWearable->GetAttributeContainer()->GetItem();
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pWearable, iDuckBadgeLevel, duck_badge_level );
+ //if ( pItem && FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pItem, pAttr_DuckLevelBadge, &iDuckBadgeLevel ) )
+ {
+ iDuckBadgeLevel++;
+ }
+ }
+ }
+ }
+ // Badges only go to max of 5 now instead of 10 so just doubling the output value
+ int iDuckPower = Min( (int)iDuckBadgeLevel * 2, 11 );
+ float flBias = RemapValClamped( (float)iDuckPower, 0.0f, 10.0f, 0.2f, 0.5f);
+ float flBiasScale = 3.0f;
+ int iMinimum = 0;
+ bool bSpecial = false;
+
+ // Drop a few bonus ducks, extra ducks for Crits!
+#ifdef STAGING_ONLY
+ flBias = RemapValClamped( (float)iDuckPower, 0.0f, 10.0f, tf_duck_droprate_bias.GetFloat(), 0.5f);
+ iMinimum = tf_duck_droprate_min.GetInt();
+ if ( tf_duck_power_override.GetInt() > 0 )
+ {
+ iDuckPower = tf_duck_power_override.GetInt();
+ }
+ flBiasScale = tf_duck_random_extra.GetFloat();
+#endif
+ //tf_duck_droprate_bias
+ int iDuckCount = (int)( Bias( RandomFloat( 0, 1 ), flBias ) * ( flBiasScale + iDuckPower ) ) + iMinimum;
+ iDuckCount = Max( iDuckCount, (int)iDuckBadgeLevel ); // min ducks for a badge
+
+ if ( bCrit )
+ {
+ iDuckCount += RandomInt( 1, 2 );
+ }
+
+ if ( pTFCreator && ( iDuckBadgeLevel > 0 ) )
+ {
+ if ( RandomInt( 0, 600 ) <= iDuckPower )
+ {
+ // MEGA BONUS DUCKS
+ iDuckCount += iDuckPower;
+ bSpecial = true;
+ }
+ }
+
+ if ( iDuckCount > 0 )
+ {
+ // Max of 50 ducks, which is a lot of ducks
+ iDuckCount = Min( 50, iDuckCount );
+
+ // High edict count, slow generation
+ if ( gEntList.NumberOfEdicts() > tf_duck_edict_warning.GetInt() )
+ {
+ Warning( "Warning: High level of Edicts, Not spawning as many ducks\n" );
+ iDuckCount /= 2;
+ }
+
+ int iCreatorId = pTFCreator ? pTFCreator->entindex() : -1;
+ int iAssisterId = pAssister ? pAssister->entindex() : -1;
+
+ int iVictimId = -1;
+ int iDuckTeam = TEAM_UNASSIGNED;
+ if ( pTFVictim )
+ {
+ iVictimId = pTFVictim->entindex();
+ iDuckTeam = pTFVictim->GetTeamNumber();
+ }
+
+ for ( int i = 0; i < iDuckCount; ++i )
+ {
+ Vector vecOrigin = vPosition + Vector( 0, 0, 50 );
+ CBonusDuckPickup *pDuckPickup = assert_cast<CBonusDuckPickup*>( CBaseEntity::CreateNoSpawn( "tf_bonus_duck_pickup", vecOrigin, vec3_angle, NULL ) );
+ if ( pDuckPickup )
+ {
+ DispatchSpawn( pDuckPickup );
+
+ Vector vecImpulse = RandomVector( -0.5f, 0.5f );
+ vecImpulse.z = 1.f;
+ VectorNormalize( vecImpulse );
+
+ Vector vecVelocity = vecImpulse * RandomFloat( 350.0f, 450.0f );
+ pDuckPickup->DropSingleInstance( vecVelocity, NULL, 1.0f );
+ pDuckPickup->SetCreatorId( iCreatorId );
+ pDuckPickup->SetVictimId( iVictimId );
+ pDuckPickup->SetAssisterId( iAssisterId );
+ pDuckPickup->ChangeTeam( iDuckTeam );
+ pDuckPickup->SetDuckFlag( iDuckFlags );
+ // random chance to have a special duck appear
+ // Bonus duck implies atleast 1 Saxton
+ if ( bSpecial || ( RandomInt( 1, 100 ) <= tf_test_special_ducks.GetInt() ) )
+ {
+ bSpecial = false;
+ pDuckPickup->m_nSkin = 21; // Quackston Hale
+ pDuckPickup->SetSpecial();
+
+ CSingleUserRecipientFilter filter( pTFCreator );
+ UserMessageBegin( filter, "BonusDucks" );
+ WRITE_BYTE( iCreatorId );
+ WRITE_BYTE( true );
+ MessageEnd();
+ }
+ else
+ {
+ if ( iDuckPower > 0 )
+ {
+ pDuckPickup->m_nSkin = GetDuckSkinForClass( iDuckTeam, ( pTFVictim && pTFVictim->GetPlayerClass() ) ? pTFVictim->GetPlayerClass()->GetClassIndex() : -1 );
+ }
+ else
+ {
+ // Scorer without a badge only make normal ducks
+ pDuckPickup->m_nSkin = iDuckTeam == TF_TEAM_RED ? 1 : 2;
+ }
+ pDuckPickup->SetModelScale( 0.7f );
+ }
+ }
+ }
+
+ // Update duckstreak count on player and assister if they have badges
+ if ( pTFCreator && PlayerHasDuckStreaks( pTFCreator ) )
+ {
+ pTFCreator->m_Shared.IncrementStreak( CTFPlayerShared::kTFStreak_Ducks, iDuckCount );
+ }
+ if ( pAssister && PlayerHasDuckStreaks( pAssister ) )
+ {
+ pAssister->m_Shared.IncrementStreak( CTFPlayerShared::kTFStreak_Ducks, iDuckCount );
+ }
+
+ // Duck UserMessage
+ if ( pTFCreator && pTFVictim && !TFGameRules()->HaveCheatsBeenEnabledDuringLevel() )
+ {
+ if ( IsHolidayActive( kHoliday_EOTL ) )
+ {
+ // Send a Message to Creator
+ {
+ // IsCreated, ID of Creator, ID of Victim, Count, IsGolden
+ CSingleUserRecipientFilter userfilter( pTFCreator );
+ UserMessageBegin( userfilter, "EOTLDuckEvent" );
+ WRITE_BYTE( true );
+ WRITE_BYTE( iCreatorId );
+ WRITE_BYTE( iVictimId );
+ WRITE_BYTE( 0 );
+ WRITE_BYTE( iDuckTeam );
+ WRITE_BYTE( iDuckCount );
+ WRITE_BYTE( iDuckFlags );
+ MessageEnd();
+ }
+
+ // To assister
+ if ( pAssister )
+ {
+ // IsCreated, ID of Creator, ID of Victim, Count, IsGolden
+ CSingleUserRecipientFilter userfilter( pAssister );
+ UserMessageBegin( userfilter, "EOTLDuckEvent" );
+ WRITE_BYTE( true );
+ WRITE_BYTE( pAssister->entindex() );
+ WRITE_BYTE( iVictimId );
+ WRITE_BYTE( 0 );
+ WRITE_BYTE( iDuckTeam );
+ WRITE_BYTE( iDuckCount );
+ WRITE_BYTE( iDuckFlags );
+ MessageEnd();
+ }
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+static kill_eater_event_t g_eClassKillEvents[] =
+{
+ kKillEaterEvent_ScoutKill, // TF_CLASS_SCOUT
+ kKillEaterEvent_SniperKill, // TF_CLASS_SNIPER
+ kKillEaterEvent_SoldierKill, // TF_CLASS_SOLDIER
+ kKillEaterEvent_DemomanKill, // TF_CLASS_DEMOMAN
+ kKillEaterEvent_MedicKill, // TF_CLASS_MEDIC
+ kKillEaterEvent_HeavyKill, // TF_CLASS_HEAVYWEAPONS
+ kKillEaterEvent_PyroKill, // TF_CLASS_PYRO
+ kKillEaterEvent_SpyKill, // TF_CLASS_SPY
+ kKillEaterEvent_EngineerKill, // TF_CLASS_ENGINEER
+};
+COMPILE_TIME_ASSERT( ARRAYSIZE( g_eClassKillEvents ) == (TF_LAST_NORMAL_CLASS - TF_FIRST_NORMAL_CLASS) );
+
+//-----------------------------------------------------------------------------
+static kill_eater_event_t g_eRobotClassKillEvents[] =
+{
+ kKillEaterEvent_RobotScoutKill, // TF_CLASS_SCOUT
+ kKillEaterEvent_RobotSniperKill, // TF_CLASS_SNIPER
+ kKillEaterEvent_RobotSoldierKill, // TF_CLASS_SOLDIER
+ kKillEaterEvent_RobotDemomanKill, // TF_CLASS_DEMOMAN
+ kKillEaterEvent_RobotMedicKill, // TF_CLASS_MEDIC
+ kKillEaterEvent_RobotHeavyKill, // TF_CLASS_HEAVYWEAPONS
+ kKillEaterEvent_RobotPyroKill, // TF_CLASS_PYRO
+ kKillEaterEvent_RobotSpyKill, // TF_CLASS_SPY
+ kKillEaterEvent_RobotEngineerKill, // TF_CLASS_ENGINEER
+};
+COMPILE_TIME_ASSERT( ARRAYSIZE( g_eRobotClassKillEvents ) == (TF_LAST_NORMAL_CLASS - TF_FIRST_NORMAL_CLASS) );
+
+static bool BHasWearableOfSpecificQualityEquipped( /*const*/ CTFPlayer *pTFPlayer, EEconItemQuality eQuality )
+{
+ Assert( pTFPlayer );
+
+ // Fire the kill eater event on all wearables
+ for ( int i = 0; i < pTFPlayer->GetNumWearables(); ++i )
+ {
+ CTFWearable *pWearableItem = dynamic_cast<CTFWearable *>( pTFPlayer->GetWearable( i ) );
+ if ( !pWearableItem )
+ continue;
+
+ if ( !pWearableItem->GetAttributeContainer() )
+ continue;
+
+ CEconItemView *pEconItemView = pWearableItem->GetAttributeContainer()->GetItem();
+ if ( !pEconItemView )
+ continue;
+
+ if ( pEconItemView->GetQuality() == eQuality )
+ return true;
+ }
+
+ return false;
+}
+
+void CTFGameRules::PlayerKilled( CBasePlayer *pVictim, const CTakeDamageInfo &info )
+{
+ // Find the killer & the scorer
+ CBaseEntity *pInflictor = info.GetInflictor();
+ CBaseEntity *pKiller = info.GetAttacker();
+ CBaseMultiplayerPlayer *pScorer = ToBaseMultiplayerPlayer( GetDeathScorer( pKiller, pInflictor, pVictim ) );
+ CTFPlayer *pAssister = NULL;
+ CBaseObject *pObject = NULL;
+
+ // if inflictor or killer is a base object, tell them that they got a kill
+ // ( depends if a sentry rocket got the kill, sentry may be inflictor or killer )
+ if ( pInflictor )
+ {
+ if ( pInflictor->IsBaseObject() )
+ {
+ pObject = dynamic_cast<CBaseObject *>( pInflictor );
+ }
+ else
+ {
+ CBaseEntity *pInflictorOwner = pInflictor->GetOwnerEntity();
+ if ( pInflictorOwner && pInflictorOwner->IsBaseObject() )
+ {
+ pObject = dynamic_cast<CBaseObject *>( pInflictorOwner );
+ }
+ }
+
+ }
+ else if( pKiller && pKiller->IsBaseObject() )
+ {
+ pObject = dynamic_cast<CBaseObject *>( pKiller );
+ }
+
+ if ( pObject )
+ {
+ if ( pObject->GetBuilder() != pVictim )
+ {
+ pObject->IncrementKills();
+
+ // minibosses count for 5 kills
+ if ( IsMannVsMachineMode() && pVictim && pVictim->IsPlayer() )
+ {
+ CTFPlayer *playerVictim = ToTFPlayer( pVictim );
+ if ( playerVictim->IsMiniBoss() )
+ {
+ pObject->IncrementKills();
+ pObject->IncrementKills();
+ pObject->IncrementKills();
+ pObject->IncrementKills();
+ }
+ }
+ }
+ pInflictor = pObject;
+
+ if ( pObject->ObjectType() == OBJ_SENTRYGUN )
+ {
+ // notify the sentry
+ CObjectSentrygun *pSentrygun = dynamic_cast<CObjectSentrygun *>( pObject );
+ if ( pSentrygun )
+ {
+ pSentrygun->OnKilledEnemy( pVictim );
+ }
+
+ CTFPlayer *pOwner = pObject->GetOwner();
+ if ( pOwner )
+ {
+ int iKills = pObject->GetKills();
+
+ // keep track of max kills per a single sentry gun in the player object
+ if ( pOwner->GetMaxSentryKills() < iKills )
+ {
+ pOwner->SetMaxSentryKills( iKills );
+ }
+
+ // if we just got 10 kills with one sentry, tell the owner's client, which will award achievement if it doesn't have it already
+ if ( iKills == 10 )
+ {
+ pOwner->AwardAchievement( ACHIEVEMENT_TF_GET_TURRETKILLS );
+ }
+ }
+ }
+ }
+
+ // if not killed by suicide or killed by world, see if the scorer had an assister, and if so give the assister credit
+ if ( ( pVictim != pScorer ) && pKiller )
+ {
+ pAssister = ToTFPlayer( GetAssister( pVictim, pScorer, pInflictor ) );
+
+ CTFPlayer *pTFVictim = ToTFPlayer( pVictim );
+ if ( pAssister && pTFVictim )
+ {
+ EntityHistory_t* entHist = pTFVictim->m_AchievementData.IsSentryDamagerInHistory( pAssister, 5.0 );
+ if ( entHist )
+ {
+ CBaseObject *pObj = dynamic_cast<CBaseObject*>( entHist->hObject.Get() );
+ if ( pObj )
+ {
+ pObj->IncrementAssists();
+ }
+ }
+
+ // Rage On Assists
+ // Sniper Kill Rage
+ if ( pAssister->IsPlayerClass( TF_CLASS_SNIPER ) )
+ {
+ // Item attribute
+ // Add Sniper Rage On Assists
+ float flRageGain = 0;
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pAssister, flRageGain, rage_on_assists );
+ if (flRageGain != 0)
+ {
+ pAssister->m_Shared.ModifyRage(flRageGain);
+ }
+ }
+ }
+ }
+
+ if ( pVictim )
+ {
+ if ( ShouldDropSpellPickup() )
+ {
+ DropSpellPickup( pVictim->GetAbsOrigin() );
+ }
+
+ CTFPlayer *pTFScorer = ToTFPlayer( pScorer );
+ CTFPlayer *pTFVictim = ToTFPlayer( pVictim );
+ if ( pTFScorer && pTFVictim )
+ {
+ if ( ShouldDropBonusDuckFromPlayer( pTFScorer, pTFVictim ) )
+ {
+ DropBonusDuck( pTFVictim->GetAbsOrigin(), pTFScorer, pAssister, pTFVictim, ( info.GetDamageType() & DMG_CRITICAL ) != 0 );
+ }
+ }
+
+ // Drop a halloween soul!
+ if ( IsHolidayActive( kHoliday_Halloween ) )
+ {
+ CBaseCombatCharacter* pBaseCombatScorer = dynamic_cast< CBaseCombatCharacter*>( pScorer ? pScorer : pKiller );
+ // No souls for a pure suicide
+ if ( pTFVictim != pBaseCombatScorer )
+ {
+ // Only spawn a soul if the target is a base combat character.
+ if ( pTFVictim && pBaseCombatScorer )
+ {
+ DropHalloweenSoulPack( 1, pVictim->EyePosition(), pBaseCombatScorer, pTFVictim->GetTeamNumber() );
+ }
+
+ // Also spawn one for the assister
+ if ( pAssister )
+ {
+ DropHalloweenSoulPack( 1, pVictim->EyePosition(), pAssister, pTFVictim->GetTeamNumber() );
+ }
+ }
+ }
+ }
+
+ //find the area the player is in and see if his death causes a block
+ CBaseMultiplayerPlayer *pMultiplayerPlayer = ToBaseMultiplayerPlayer(pVictim);
+ for ( int i=0; i<ITriggerAreaCaptureAutoList::AutoList().Count(); ++i )
+ {
+ CTriggerAreaCapture *pArea = static_cast< CTriggerAreaCapture * >( ITriggerAreaCaptureAutoList::AutoList()[i] );
+ if ( pArea->IsTouching( pMultiplayerPlayer ) )
+ {
+ // ACHIEVEMENT_TF_MEDIC_ASSIST_CAPTURER
+ // kill 3 players
+ if ( pAssister && pAssister->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ // the victim is touching a cap point, see if they own it
+ if ( pMultiplayerPlayer->GetTeamNumber() == pArea->GetOwningTeam() )
+ {
+ int iDefenderKills = pAssister->GetPerLifeCounterKV( "medic_defender_kills" );
+
+ if ( ++iDefenderKills == 3 )
+ {
+ pAssister->AwardAchievement( ACHIEVEMENT_TF_MEDIC_ASSIST_CAPTURER );
+ }
+
+ pAssister->SetPerLifeCounterKV( "medic_defender_kills", iDefenderKills );
+ }
+ }
+
+ if ( pArea->CheckIfDeathCausesBlock( pMultiplayerPlayer, pScorer ) )
+ break;
+ }
+ }
+
+ // determine if this kill affected a nemesis relationship
+ int iDeathFlags = 0;
+ CTFPlayer *pTFPlayerVictim = ToTFPlayer( pVictim );
+ CTFPlayer *pTFPlayerScorer = ToTFPlayer( pScorer );
+ if ( pScorer )
+ {
+ CalcDominationAndRevenge( pTFPlayerScorer, info.GetWeapon(), pTFPlayerVictim, false, &iDeathFlags );
+ if ( pAssister )
+ {
+ CalcDominationAndRevenge( pAssister, info.GetWeapon(), pTFPlayerVictim, true, &iDeathFlags );
+ }
+ }
+ pTFPlayerVictim->SetDeathFlags( iDeathFlags );
+
+ CTFPlayer *pTFPlayerAssister = ToTFPlayer( pAssister );
+ if ( pTFPlayerAssister )
+ {
+ CTF_GameStats.Event_AssistKill( pTFPlayerAssister, pVictim );
+ }
+
+ // check for achievements
+ PlayerKilledCheckAchievements( pTFPlayerScorer, pTFPlayerVictim );
+
+ if ( IsInTraining() && GetTrainingModeLogic() )
+ {
+ if ( pVictim->IsFakeClient() == false )
+ {
+ GetTrainingModeLogic()->OnPlayerDied( ToTFPlayer( pVictim ), pKiller );
+ }
+ else
+ {
+ GetTrainingModeLogic()->OnBotDied( ToTFPlayer( pVictim ), pKiller );
+ }
+ }
+
+ // credit for dueling
+ if ( pTFPlayerScorer != NULL && pTFPlayerScorer != pTFPlayerVictim )
+ {
+ DuelMiniGame_NotifyKill( pTFPlayerScorer, pTFPlayerVictim );
+ }
+ if ( pTFPlayerAssister && pTFPlayerAssister != pTFPlayerVictim )
+ {
+ DuelMiniGame_NotifyAssist( pTFPlayerAssister, pTFPlayerVictim );
+ }
+
+ // Count kills from powerup carriers to detect imbalances
+ if ( IFuncPowerupVolumeAutoList::AutoList().Count() != 0 ) // If there are no func_powerupvolumes in the map, there's no point in checking for imbalances
+ {
+ if ( pTFPlayerScorer && pTFPlayerScorer != pTFPlayerVictim && pTFPlayerScorer->m_Shared.IsCarryingRune() && !pTFPlayerScorer->m_Shared.InCond( TF_COND_RUNE_IMBALANCE ) )
+ {
+ if ( !m_bPowerupImbalanceMeasuresRunning && !PowerupModeFlagStandoffActive() ) // Only count if imbalance measures aren't running and there is no flag standoff
+ {
+ if ( pTFPlayerScorer->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ m_nPowerupKillsBlueTeam++; // Blue team score increases if a powered up blue player makes a kill
+ }
+ else if ( pTFPlayerScorer->GetTeamNumber() == TF_TEAM_RED )
+ {
+ m_nPowerupKillsRedTeam++;
+ }
+
+ int nBlueAdvantage = m_nPowerupKillsBlueTeam - m_nPowerupKillsRedTeam; // How far ahead is this team?
+// Msg( "\nnBlueAdvantage = %d\n", nBlueAdvantage );
+ int nRedAdvantage = m_nPowerupKillsRedTeam - m_nPowerupKillsBlueTeam;
+// Msg( "nRedAdvantage = %d\n\n", nRedAdvantage );
+
+ if ( nRedAdvantage >= tf_powerup_mode_imbalance_delta.GetInt() )
+ {
+ PowerupTeamImbalance( TF_TEAM_BLUE ); // Fire the output to map logic
+ }
+ else if ( nBlueAdvantage >= tf_powerup_mode_imbalance_delta.GetInt() )
+ {
+ PowerupTeamImbalance( TF_TEAM_RED );
+ }
+ }
+ }
+ }
+
+ // credit for kill-eating weapons and anything else that might care
+ if ( pTFPlayerScorer && pTFPlayerVictim && pTFPlayerScorer != pTFPlayerVictim )
+ {
+ // Increment the server-side kill count for this weapon -- this is used for honorbound
+ // weapons and has nothing to do with strange weapons/stats.
+ pTFPlayerScorer->IncrementKillCountSinceLastDeploy( info );
+
+ CTFWeaponBase *pTFWeapon = GetKilleaterWeaponFromDamageInfo( &info );
+
+ kill_eater_event_t eKillEaterEvent = pTFWeapon // if we have a weapon...
+ ? pTFWeapon->GetKillEaterKillEventType() // ...then ask it what type of event to report
+ : info.GetDamageCustom() == TF_DMG_CUSTOM_BOOTS_STOMP // if we don't have a weapon, and we're hacking for kill types from wearables (!)...
+ ? kKillEaterEvent_PlayerKillByBootStomp // ...then use our hard-coded event
+ : kKillEaterEvent_PlayerKill; // otherwise default to a normal player kill
+
+ CEconEntity *pAttackerEconWeapon = NULL;
+
+ // Cosmetic Kill like manntreads or demo shield
+ if ( !pTFWeapon )
+ {
+ pAttackerEconWeapon = dynamic_cast< CEconEntity * >( info.GetWeapon() );
+ }
+ else
+ {
+ pAttackerEconWeapon = dynamic_cast< CEconEntity * >( pTFWeapon );
+ }
+
+
+ if ( pAttackerEconWeapon )
+ {
+ if ( !( IsPVEModeActive() && pTFPlayerVictim->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) )
+ {
+ // Any type of non-robot kill!
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, eKillEaterEvent );
+
+ // Cosmetic any kill type tracking
+ {
+ HatAndMiscEconEntities_OnOwnerKillEaterEvent( pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_CosmeticKills );
+
+ // Halloween silliness: overworld kills for Merasmus carnival.
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_DOOMSDAY ) && !pTFPlayerScorer->m_Shared.InCond( TF_COND_HALLOWEEN_IN_HELL ) )
+ {
+ HatAndMiscEconEntities_OnOwnerKillEaterEvent( pTFPlayerScorer,
+ pTFPlayerVictim,
+ kKillEaterEvent_Halloween_OverworldKills );
+ }
+ }
+
+ // Operation Kills
+ EconEntity_NonEquippedItemKillTracking( pTFPlayerScorer, pTFPlayerVictim, 1 );
+
+ // Unique kill tracking?
+ EconEntity_OnOwnerUniqueEconEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_UniqueEvent__KilledAccountWithItem );
+
+ // Optional Taunt Kill tracking
+ if ( IsTauntDmg( info.GetDamageCustom() ) )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_TauntKill );
+ }
+ // Scorch Shot Taunt is Different
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_FLARE_PELLET )
+ {
+ CBaseEntity *pInflictor = info.GetInflictor();
+ CTFProjectile_Flare *pFlare = dynamic_cast<CTFProjectile_Flare*>(pInflictor);
+ if ( pFlare && pFlare->IsFromTaunt() )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_TauntKill );
+ }
+ }
+
+ // Optional: also track "killed X players of this specific class".
+ int iVictimClassIndex = pTFPlayerVictim->GetPlayerClass()->GetClassIndex();
+ if ( iVictimClassIndex >= TF_FIRST_NORMAL_CLASS && iVictimClassIndex <= TF_LAST_NORMAL_CLASS )
+ {
+ const kill_eater_event_t eClassKillType = g_eClassKillEvents[ iVictimClassIndex - TF_FIRST_NORMAL_CLASS ];
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, eClassKillType );
+ }
+
+ // Optional : Kills while in Victory / Bonus time
+ if ( State_Get() == GR_STATE_TEAM_WIN && GetWinningTeam() != pTFPlayerVictim->GetTeamNumber() )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_VictoryTimeKill );
+ }
+
+ // Optional: also track "killed X players while they were in the air".
+ if ( !(pTFPlayerVictim->GetFlags() & FL_ONGROUND) && (pTFPlayerVictim->GetWaterLevel() == WL_NotInWater) )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_AirborneEnemyKill );
+ }
+
+ // Optional: also track "killed X players with headshots".
+ if ( IsHeadshot( info.GetDamageCustom() ) )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_HeadshotKill );
+ }
+
+ // Optional: also track "gibbed X players".
+ if ( pTFPlayerVictim->ShouldGib( info ) )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_GibKill );
+ }
+
+ // Optional: also track "killed X players during full moons". We intentionally don't call into the
+ // game rules here because we don't want server variables to override this. Setting local time on the
+ // server will still allow it to happen but it at least takes a little effort.
+ if ( UTIL_IsHolidayActive( kHoliday_FullMoon ) )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_PlayerKillDuringFullMoon );
+ }
+
+ // Optional: also track kills we make while we're dead (afterburn, pre-death-fired rocket, etc.)
+ if ( !pTFPlayerScorer->IsAlive() )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_PlayerKillPosthumous );
+ }
+
+ // Optional: also track kills that were specifically criticals.
+ if ( (info.GetDamageType() & DMG_CRITICAL) != 0 )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_PlayerKillCritical );
+ }
+ else
+ {
+ // Not special at all kill
+ if ( pTFPlayerVictim->GetAttackBonusEffect() == kBonusEffect_None )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_NonCritKills );
+ }
+ }
+
+ // Optional: also track kills that were made while we were launched into the air from an explosion (ie., rocket-jumping).
+ if ( pTFPlayerScorer->InAirDueToExplosion() )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_PlayerKillWhileExplosiveJumping );
+ }
+
+ // Optional: also track kills where the victim was a spy who was invisible at the time of death.
+ if ( iVictimClassIndex == TF_CLASS_SPY && pTFPlayerVictim->m_Shared.GetPercentInvisible() > 0 )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_InvisibleSpiesKilled );
+ }
+
+ // Optional: also track kills where the victim was a medic with 100% uber.
+ if ( pTFPlayerVictim->MedicGetChargeLevel() >= 1.0f )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_MedicsWithFullUberKilled );
+ }
+
+ // Optional: also track kills where the killer was at low health when they dealt the final damage. Don't count
+ // kills with 0 or fewer health -- those would be post-mortem kills instead.
+ if ( ((float)pTFPlayerScorer->GetHealth() / (float)pTFPlayerScorer->GetMaxHealth()) <= 0.1f && pTFPlayerScorer->GetHealth() > 0 )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_KillWhileLowHealth );
+ }
+
+ // Optional: also track kills that take place during the Halloween holiday. We intentionally *do* check against
+ // the game rules logic here -- if folks want to enable Halloween mode on a server and play around, I don't see any
+ // reason why we benefit from stopping their fun.
+ if ( TF_IsHolidayActive( kHoliday_Halloween ) )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_HalloweenKill );
+ }
+
+ // Optional: also track kills where the victim was completely underwater.
+ if ( pTFPlayerVictim->GetWaterLevel() >= WL_Eyes )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_UnderwaterKill );
+ }
+
+ // Optional: also track kills where we're under the effects of a medic's ubercharge.
+ if ( pTFPlayerScorer->m_Shared.InCond( TF_COND_INVULNERABLE ) || pTFPlayerScorer->m_Shared.InCond( TF_COND_INVULNERABLE_WEARINGOFF ) )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_KillWhileUbercharged );
+ }
+
+ // Optional: also track kills where the victim was in the process of carrying the intel, capturing a point, or pushing
+ // the cart. The actual logic for "killed a flag carrier" is handled elsewhere because by this point we've forgotten
+ // if we had a flag before we died.
+ if ( pTFPlayerVictim->IsCapturingPoint() )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_DefenderKill );
+ }
+
+ // Optional: also track kills where the victim was at least N units from the person dealing the damage, where N is "however far
+ // you have to be to get the crowd chear noise". This also specifically checks to make sure the damage dealer is alive to avoid
+ // casing where you spectate far away, etc.
+ if ( pTFPlayerScorer->IsAlive() && (pTFPlayerScorer->GetAbsOrigin() - pTFPlayerVictim->GetAbsOrigin()).Length() >= 2000.0f )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_LongDistanceKill );
+ }
+
+ // Optional: also track kills where the victim was wearing at least one unusual-quality item.
+ if ( BHasWearableOfSpecificQualityEquipped( pTFPlayerVictim, AE_UNUSUAL ) )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_PlayersWearingUnusualKill );
+ }
+
+ // Optional: also track kills where the victim was on fire at the time that they died. We can't check the condition flag
+ // here because that may or may not be active after they die, but instead we test to see whether they still had a burn queued
+ // up for the future as a proxy for "was on fire at this point".
+ if ( pTFPlayerVictim->m_Shared.GetFlameBurnTime() >= gpGlobals->curtime )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_BurningEnemyKill );
+ }
+
+ // Optional: also track kills where this kill ended someone else's killstreak, where "killstreak" starts when the first
+ // announcement happens. If Mike gets to hard-code constants, I do, too.
+ enum { kFirstKillStreakAnnouncement = 5 };
+ if ( pTFPlayerVictim->m_Shared.GetStreak( CTFPlayerShared::kTFStreak_Kills ) >= kFirstKillStreakAnnouncement )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_KillstreaksEnded );
+ }
+
+ // Optional: also track kills where the killer and the victim were basically right next to each other. This is hard-coded to
+ // 1.5x default melee swing range.
+ if ( pTFPlayerScorer->IsAlive() && (pTFPlayerScorer->GetAbsOrigin() - pTFPlayerVictim->GetAbsOrigin()).Length() <= 72.0f )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_PointBlankKills );
+ }
+
+ // Optional : Fullhealth kills
+ if ( pTFPlayerScorer->GetHealth() >= pTFPlayerScorer->GetMaxHealth() )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_FullHealthKills );
+ }
+
+ // Optional : Taunting Player kills
+ if ( pTFPlayerVictim->IsTaunting() )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_TauntingPlayerKills );
+ }
+ }
+ else
+ {
+ Assert( pTFPlayerVictim->GetTeamNumber() == TF_TEAM_PVE_INVADERS );
+
+ // Optional: also track kills where the victim was a robot in MvM
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_RobotsDestroyed );
+
+ // Optional: also track "killed X Robots of this specific class".
+ int iVictimClassIndex = pTFPlayerVictim->GetPlayerClass()->GetClassIndex();
+ if ( iVictimClassIndex >= TF_FIRST_NORMAL_CLASS && iVictimClassIndex <= TF_LAST_NORMAL_CLASS )
+ {
+ const kill_eater_event_t eClassKillType = g_eRobotClassKillEvents[ iVictimClassIndex - TF_FIRST_NORMAL_CLASS ];
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, eClassKillType );
+ }
+
+ // Optional: specifically track miniboss kills separately from base robot kills.
+ if ( pTFPlayerVictim->IsMiniBoss() )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_MinibossRobotsDestroyed );
+ }
+
+ // Optional: track whether this shot killed a robot *after* penetrating something else.
+ if ( info.GetPlayerPenetrationCount() > 0 )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_RobotsDestroyedAfterPenetration );
+ }
+
+ // Optional: also track "killed X players with headshots", but for robots specifically.
+ if ( IsHeadshot( info.GetDamageCustom() ) )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_RobotHeadshotKills );
+ }
+
+ // Optional: also track kills that take place during the Halloween holiday. See note for kKillEaterEvent_HalloweenKill
+ // regarding calling into game rules here.
+ if ( TF_IsHolidayActive( kHoliday_Halloween ) )
+ {
+ EconEntity_OnOwnerKillEaterEvent( pAttackerEconWeapon, pTFPlayerScorer, pTFPlayerVictim, kKillEaterEvent_HalloweenKillRobot );
+ }
+ }
+ }
+ }
+
+ BaseClass::PlayerKilled( pVictim, info );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::PlayerKilledCheckAchievements( CTFPlayer *pAttacker, CTFPlayer *pVictim )
+{
+ if ( !pAttacker || !pVictim )
+ return;
+
+ // HEAVY WEAPONS GUY
+ if ( pAttacker->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) )
+ {
+ if ( GetGameType() == TF_GAMETYPE_CP )
+ {
+ // ACHIEVEMENT_TF_HEAVY_DEFEND_CONTROL_POINT
+ CTriggerAreaCapture *pAreaTrigger = pAttacker->GetControlPointStandingOn();
+ if ( pAreaTrigger )
+ {
+ CTeamControlPoint *pCP = pAreaTrigger->GetControlPoint();
+ if ( pCP )
+ {
+ if ( pCP->GetOwner() == pAttacker->GetTeamNumber() )
+ {
+ // no suicides!
+ if ( pAttacker != pVictim )
+ {
+ pAttacker->AwardAchievement( ACHIEVEMENT_TF_HEAVY_DEFEND_CONTROL_POINT );
+ }
+ }
+ }
+ }
+
+ // ACHIEVEMENT_TF_HEAVY_KILL_CAPPING_ENEMIES
+ pAreaTrigger = pVictim->GetControlPointStandingOn();
+ if ( pAreaTrigger )
+ {
+ CTeamControlPoint *pCP = pAreaTrigger->GetControlPoint();
+ if ( pCP )
+ {
+ if ( pCP->GetOwner() == pAttacker->GetTeamNumber() &&
+ TeamMayCapturePoint( pVictim->GetTeamNumber(), pCP->GetPointIndex() ) &&
+ PlayerMayCapturePoint( pVictim, pCP->GetPointIndex() ) )
+ {
+ pAttacker->AwardAchievement( ACHIEVEMENT_TF_HEAVY_KILL_CAPPING_ENEMIES );
+ }
+ }
+ }
+ }
+
+ // ACHIEVEMENT_TF_HEAVY_CLEAR_STICKYBOMBS
+ if ( pVictim->IsPlayerClass( TF_CLASS_DEMOMAN ) )
+ {
+ int iPipes = pVictim->GetNumActivePipebombs();
+
+ for (int i = 0; i < iPipes; i++)
+ {
+ pAttacker->AwardAchievement( ACHIEVEMENT_TF_HEAVY_CLEAR_STICKYBOMBS );
+ }
+ }
+
+ // ACHIEVEMENT_TF_HEAVY_DEFEND_MEDIC
+ int i;
+ int iNumHealers = pAttacker->m_Shared.GetNumHealers();
+ for ( i = 0 ; i < iNumHealers ; i++ )
+ {
+ CTFPlayer *pMedic = ToTFPlayer( pAttacker->m_Shared.GetHealerByIndex( i ) );
+ if ( pMedic && pMedic->m_AchievementData.IsDamagerInHistory( pVictim, 3.0 ) )
+ {
+ pAttacker->AwardAchievement( ACHIEVEMENT_TF_HEAVY_DEFEND_MEDIC );
+ break; // just award it once for each kill...even if the victim attacked more than one medic attached to the killer
+ }
+ }
+
+ // ACHIEVEMENT_TF_HEAVY_STAND_NEAR_DISPENSER
+ for ( i = 0 ; i < iNumHealers ; i++ )
+ {
+ if ( pAttacker->m_Shared.HealerIsDispenser( i ) )
+ {
+ pAttacker->AwardAchievement( ACHIEVEMENT_TF_HEAVY_STAND_NEAR_DISPENSER );
+ break; // just award it once for each kill...even if the attacker is being healed by more than one dispenser
+ }
+ }
+ }
+
+ int i;
+ int iNumHealers = pAttacker->m_Shared.GetNumHealers();
+ for ( i = 0 ; i < iNumHealers ; i++ )
+ {
+ CTFPlayer *pMedic = ToTFPlayer( pAttacker->m_Shared.GetHealerByIndex( i ) );
+ if ( pMedic && pMedic->m_AchievementData.IsDamagerInHistory( pVictim, 10.0 ) )
+ {
+ IGameEvent * event = gameeventmanager->CreateEvent( "medic_defended" );
+ if ( event )
+ {
+ event->SetInt( "userid", pAttacker->GetUserID() );
+ event->SetInt( "medic", pMedic->GetUserID() );
+ gameeventmanager->FireEvent( event );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Determines if attacker and victim have gotten domination or revenge
+//-----------------------------------------------------------------------------
+void CTFGameRules::CalcDominationAndRevenge( CTFPlayer *pAttacker, CBaseEntity *pWeapon, CTFPlayer *pVictim, bool bIsAssist, int *piDeathFlags )
+{
+//=============================================================================
+// HPE_BEGIN:
+// [msmith] If we're in training, we want to disable this domination stuff.
+//=============================================================================
+ if ( IsInTraining() )
+ return;
+//=============================================================================
+// HPE_END
+//=============================================================================
+
+ // don't do domination stuff in powerup mode
+ if ( IsPowerupMode() )
+ return;
+
+ // no dominations/revenge in competitive mode
+ if ( IsMatchTypeCompetitive() )
+ return;
+
+ PlayerStats_t *pStatsVictim = CTF_GameStats.FindPlayerStats( pVictim );
+
+ // no dominations/revenge in PvE mode
+ if ( IsPVEModeActive() )
+ return;
+
+ CEconEntity *pEconWeapon = dynamic_cast<CEconEntity *>( pWeapon );
+
+ // calculate # of unanswered kills between killer & victim - add 1 to include current kill
+ int iKillsUnanswered = pStatsVictim->statsKills.iNumKilledByUnanswered[pAttacker->entindex()] + 1;
+ if ( TF_KILLS_DOMINATION == iKillsUnanswered )
+ {
+ // this is the Nth unanswered kill between killer and victim, killer is now dominating victim
+ *piDeathFlags |= ( bIsAssist ? TF_DEATH_ASSISTER_DOMINATION : TF_DEATH_DOMINATION );
+ // set victim to be dominated by killer
+ pAttacker->m_Shared.SetPlayerDominated( pVictim, true );
+
+ int iCurrentlyDominated = pAttacker->GetNumberofDominations() + 1;
+ pAttacker->SetNumberofDominations( iCurrentlyDominated );
+
+ // record stats
+ CTF_GameStats.Event_PlayerDominatedOther( pAttacker );
+
+ // strange weapon stat tracking?
+ EconEntity_OnOwnerKillEaterEvent( pEconWeapon, pAttacker, pVictim, kKillEaterEvent_PlayerKillStartDomination );
+ }
+ else if ( pVictim->m_Shared.IsPlayerDominated( pAttacker->entindex() ) )
+ {
+ // the killer killed someone who was dominating him, gains revenge
+ *piDeathFlags |= ( bIsAssist ? TF_DEATH_ASSISTER_REVENGE : TF_DEATH_REVENGE );
+ // set victim to no longer be dominating the killer
+ pVictim->m_Shared.SetPlayerDominated( pAttacker, false );
+
+ int iCurrentlyDominated = pVictim->GetNumberofDominations() - 1;
+ if (iCurrentlyDominated < 0)
+ {
+ iCurrentlyDominated = 0;
+ }
+ pVictim->SetNumberofDominations( iCurrentlyDominated );
+
+ // record stats
+ CTF_GameStats.Event_PlayerRevenge( pAttacker );
+
+ // strange weapon stat tracking?
+ EconEntity_OnOwnerKillEaterEvent( pEconWeapon, pAttacker, pVictim, kKillEaterEvent_PlayerKillRevenge );
+ }
+ else if ( pAttacker->m_Shared.IsPlayerDominated( pVictim->entindex() ) )
+ {
+ // the killer killed someone who they were already dominating
+
+ // strange weapon stat tracking?
+ EconEntity_OnOwnerKillEaterEvent( pEconWeapon, pAttacker, pVictim, kKillEaterEvent_PlayerKillAlreadyDominated );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: create some proxy entities that we use for transmitting data */
+//-----------------------------------------------------------------------------
+void CTFGameRules::CreateStandardEntities()
+{
+ // Create the player resource
+ g_pPlayerResource = (CPlayerResource*)CBaseEntity::Create( "tf_player_manager", vec3_origin, vec3_angle );
+
+ // Create the objective resource
+ g_pObjectiveResource = (CTFObjectiveResource *)CBaseEntity::Create( "tf_objective_resource", vec3_origin, vec3_angle );
+
+ Assert( g_pObjectiveResource );
+
+ // Create the monster resource for PvE battles
+ g_pMonsterResource = (CMonsterResource *)CBaseEntity::Create( "monster_resource", vec3_origin, vec3_angle );
+
+ MannVsMachineStats_Init();
+
+ // Create the entity that will send our data to the client.
+ m_hGamerulesProxy = assert_cast<CTFGameRulesProxy*>(CBaseEntity::Create( "tf_gamerules", vec3_origin, vec3_angle ));
+ Assert( m_hGamerulesProxy.Get() );
+ m_hGamerulesProxy->SetName( AllocPooledString("tf_gamerules" ) );
+
+ CBaseEntity::Create("vote_controller", vec3_origin, vec3_angle);
+ // Vote Issue classes are handled/cleaned-up by g_voteController
+ new CKickIssue;
+ new CRestartGameIssue;
+ new CChangeLevelIssue;
+ new CNextLevelIssue;
+ new CExtendLevelIssue;
+ new CScrambleTeams;
+ new CMannVsMachineChangeChallengeIssue;
+ new CEnableTemporaryHalloweenIssue;
+ new CTeamAutoBalanceIssue;
+ new CClassLimitsIssue;
+ new CPauseGameIssue;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: determine the class name of the weapon that got a kill
+//-----------------------------------------------------------------------------
+const char *CTFGameRules::GetKillingWeaponName( const CTakeDamageInfo &info, CTFPlayer *pVictim, int *iWeaponID )
+{
+ CBaseEntity *pInflictor = info.GetInflictor();
+ CBaseEntity *pKiller = info.GetAttacker();
+ CBasePlayer *pScorer = GetDeathScorer( pKiller, pInflictor, pVictim );
+
+ const char *killer_weapon_name = "world";
+ *iWeaponID = TF_WEAPON_NONE;
+
+ if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BURNING )
+ {
+ // special-case burning damage, since persistent burning damage may happen after attacker has switched weapons
+ killer_weapon_name = "tf_weapon_flamethrower";
+ *iWeaponID = TF_WEAPON_FLAMETHROWER;
+
+ CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( info.GetWeapon() );
+ if ( pWeapon )
+ {
+ CEconItemView *pItem = pWeapon->GetAttributeContainer()->GetItem();
+ if ( pItem && pItem->GetStaticData() && pItem->GetStaticData()->GetIconClassname() )
+ {
+ killer_weapon_name = pItem->GetStaticData()->GetIconClassname();
+ *iWeaponID = TF_WEAPON_NONE;
+ }
+ }
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BURNING_FLARE )
+ {
+ // special-case burning damage, since persistent burning damage may happen after attacker has switched weapons
+ killer_weapon_name = "tf_weapon_flaregun";
+ *iWeaponID = TF_WEAPON_FLAREGUN;
+
+ if ( pInflictor && pInflictor->IsPlayer() == false )
+ {
+ CTFBaseRocket *pBaseRocket = dynamic_cast<CTFBaseRocket*>( pInflictor );
+
+ if ( pBaseRocket )
+ {
+ if ( pBaseRocket->GetDeflected() )
+ {
+ killer_weapon_name = "deflect_flare";
+ }
+ }
+ }
+
+ CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( info.GetWeapon() );
+ if ( pWeapon && pWeapon->GetWeaponID() == TF_WEAPON_FLAREGUN_REVENGE )
+ {
+ CEconItemView *pItem = pWeapon->GetAttributeContainer()->GetItem();
+ if ( pItem && pItem->GetStaticData() && pItem->GetStaticData()->GetIconClassname() )
+ {
+ killer_weapon_name = pItem->GetStaticData()->GetIconClassname();
+ *iWeaponID = pWeapon->GetWeaponID();
+ }
+ }
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_FLARE_EXPLOSION )
+ {
+ killer_weapon_name = "tf_weapon_detonator";
+ *iWeaponID = TF_WEAPON_FLAREGUN;
+
+ if ( pInflictor && pInflictor->IsPlayer() == false )
+ {
+ CTFBaseRocket *pBaseRocket = dynamic_cast<CTFBaseRocket*>( pInflictor );
+ if ( pBaseRocket )
+ {
+ if ( pBaseRocket->GetDeflected() )
+ {
+ killer_weapon_name = "deflect_flare_detonator";
+ }
+ }
+ }
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_CHARGE_IMPACT )
+ {
+ CTFWearable *pWearable = dynamic_cast< CTFWearable * >( info.GetWeapon() );
+ if ( pWearable )
+ {
+ CEconItemView *pItem = pWearable->GetAttributeContainer()->GetItem();
+ if ( pItem && pItem->GetStaticData() && pItem->GetStaticData()->GetIconClassname() )
+ {
+ killer_weapon_name = pItem->GetStaticData()->GetIconClassname();
+ *iWeaponID = TF_WEAPON_NONE;
+ }
+ }
+ }
+ else if ( info.GetPlayerPenetrationCount() > 0 )
+ {
+ // This may be stomped later if the kill was a headshot.
+ killer_weapon_name = "player_penetration";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_PICKAXE )
+ {
+ killer_weapon_name = "pickaxe";
+ *iWeaponID = TF_WEAPON_SHOVEL;
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_CARRIED_BUILDING )
+ {
+ killer_weapon_name = "tf_weapon_building_carried_destroyed";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_HADOUKEN )
+ {
+ killer_weapon_name = "tf_weapon_taunt_pyro";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_HIGH_NOON )
+ {
+ killer_weapon_name = "tf_weapon_taunt_heavy";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_GRAND_SLAM )
+ {
+ killer_weapon_name = "tf_weapon_taunt_scout";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_BARBARIAN_SWING )
+ {
+ killer_weapon_name = "tf_weapon_taunt_demoman";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_UBERSLICE )
+ {
+ killer_weapon_name = "tf_weapon_taunt_medic";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_FENCING )
+ {
+ killer_weapon_name = "tf_weapon_taunt_spy";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_ARROW_STAB )
+ {
+ killer_weapon_name = "tf_weapon_taunt_sniper";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_GRENADE )
+ {
+ CTFPlayer *pTFKiller = ToTFPlayer( pKiller );
+ if ( pTFKiller && pTFKiller->IsWormsGearEquipped() )
+ {
+ killer_weapon_name = "tf_weapon_taunt_soldier_lumbricus";
+ }
+ else
+ {
+ killer_weapon_name = "tf_weapon_taunt_soldier";
+ }
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_ENGINEER_GUITAR_SMASH )
+ {
+ killer_weapon_name = "tf_weapon_taunt_guitar_kill";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_ALLCLASS_GUITAR_RIFF )
+ {
+ killer_weapon_name = "tf_weapon_taunt_guitar_riff_kill";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_ENGINEER_ARM_KILL )
+ {
+ killer_weapon_name = "robot_arm_blender_kill";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TELEFRAG )
+ {
+ killer_weapon_name = "telefrag";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BOOTS_STOMP )
+ {
+ killer_weapon_name = "mantreads";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BASEBALL )
+ {
+ killer_weapon_name = "ball";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_COMBO_PUNCH )
+ {
+ killer_weapon_name = "robot_arm_combo_kill";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BLEEDING )
+ {
+ killer_weapon_name = "tf_weapon_bleed_kill";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_PLAYER_SENTRY )
+ {
+ int nGigerCounter = 0;
+ if ( pScorer )
+ {
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pScorer, nGigerCounter, is_giger_counter );
+ }
+
+ if ( nGigerCounter > 0 )
+ {
+ killer_weapon_name = "giger_counter";
+ }
+ else
+ {
+ killer_weapon_name = "wrangler_kill";
+ }
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_DECAPITATION_BOSS )
+ {
+ killer_weapon_name = "headtaker";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_EYEBALL_ROCKET )
+ {
+ killer_weapon_name = "eyeball_rocket";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_STICKBOMB_EXPLOSION )
+ {
+ killer_weapon_name = "ullapool_caber_explosion";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_TAUNTATK_ARMAGEDDON )
+ {
+ killer_weapon_name = "armageddon";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SAPPER_RECORDER_DEATH )
+ {
+ killer_weapon_name = "recorder";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_MERASMUS_DECAPITATION )
+ {
+ killer_weapon_name = "merasmus_decap";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_MERASMUS_ZAP )
+ {
+ killer_weapon_name = "merasmus_zap";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_MERASMUS_GRENADE )
+ {
+ killer_weapon_name = "merasmus_grenade";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_MERASMUS_PLAYER_BOMB )
+ {
+ killer_weapon_name = "merasmus_player_bomb";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_CANNONBALL_PUSH )
+ {
+ killer_weapon_name = "loose_cannon_impact";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SPELL_TELEPORT )
+ {
+ killer_weapon_name = "spellbook_teleport";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SPELL_SKELETON )
+ {
+ killer_weapon_name = "spellbook_skeleton";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SPELL_MIRV )
+ {
+ killer_weapon_name = "spellbook_mirv";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SPELL_METEOR )
+ {
+ killer_weapon_name = "spellbook_meteor";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SPELL_LIGHTNING )
+ {
+ killer_weapon_name = "spellbook_lightning";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SPELL_FIREBALL )
+ {
+ killer_weapon_name = "spellbook_fireball";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SPELL_MONOCULUS )
+ {
+ killer_weapon_name = "spellbook_boss";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SPELL_BLASTJUMP )
+ {
+ killer_weapon_name = "spellbook_blastjump";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SPELL_BATS )
+ {
+ killer_weapon_name = "spellbook_bats";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_SPELL_TINY )
+ {
+ killer_weapon_name = "spellbook_athletic";
+ }
+ else if ( info.GetDamageCustom() == TF_DMG_CUSTOM_THROWABLE ||
+ info.GetDamageCustom() == TF_DMG_CUSTOM_THROWABLE_KILL ) // Throwables
+ {
+ if ( pVictim && pVictim->GetHealth() <= 0 )
+ {
+ killer_weapon_name = "water_balloon_kill";
+ }
+ else
+ {
+ killer_weapon_name = "water_balloon_hit";
+ }
+ *iWeaponID = TF_WEAPON_THROWABLE;
+ }
+ else if ( pScorer && pInflictor && ( pInflictor == pScorer ) )
+ {
+ // If this is not a suicide
+ if ( pVictim != pScorer )
+ {
+ // If the inflictor is the killer, then it must be their current weapon doing the damage
+ if ( pScorer->GetActiveWeapon() )
+ {
+ killer_weapon_name = pScorer->GetActiveWeapon()->GetClassname();
+ if ( pScorer->IsPlayer() )
+ {
+ *iWeaponID = ToTFPlayer(pScorer)->GetActiveTFWeapon()->GetWeaponID();
+ }
+ }
+ }
+ }
+ else if ( pInflictor )
+ {
+ killer_weapon_name = STRING( pInflictor->m_iClassname );
+
+ CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( pInflictor );
+ if ( pWeapon )
+ {
+ *iWeaponID = pWeapon->GetWeaponID();
+ }
+ else
+ {
+ CTFBaseRocket *pBaseRocket = dynamic_cast<CTFBaseRocket*>( pInflictor );
+ if ( pBaseRocket )
+ {
+ *iWeaponID = pBaseRocket->GetWeaponID();
+
+ if ( pBaseRocket->GetDeflected() )
+ {
+ if ( *iWeaponID == TF_WEAPON_ROCKETLAUNCHER || *iWeaponID == TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT )
+ {
+ killer_weapon_name = "deflect_rocket";
+ }
+ else if ( *iWeaponID == TF_WEAPON_COMPOUND_BOW )
+ {
+ CTFProjectile_Arrow* pArrow = dynamic_cast<CTFProjectile_Arrow*>( pBaseRocket );
+ if ( pArrow && pArrow->IsAlight() )
+ {
+ killer_weapon_name = "deflect_huntsman_flyingburn";
+ }
+ else
+ {
+ killer_weapon_name = "deflect_arrow";
+ }
+ }
+ else if ( *iWeaponID == TF_WEAPON_SHOTGUN_BUILDING_RESCUE )
+ {
+ killer_weapon_name = "rescue_ranger_reflect";
+ }
+ }
+ else if ( *iWeaponID == TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT )
+ {
+ killer_weapon_name = "rocketlauncher_directhit";
+ }
+ }
+ else
+ {
+ CTFWeaponBaseGrenadeProj *pBaseGrenade = dynamic_cast<CTFWeaponBaseGrenadeProj*>( pInflictor );
+ if ( pBaseGrenade )
+ {
+ *iWeaponID = pBaseGrenade->GetWeaponID();
+
+ if ( pBaseGrenade->GetDeflected() )
+ {
+ if ( *iWeaponID == TF_WEAPON_GRENADE_PIPEBOMB )
+ {
+ killer_weapon_name = "deflect_sticky";
+ }
+ else if ( *iWeaponID == TF_WEAPON_GRENADE_DEMOMAN )
+ {
+ killer_weapon_name = "deflect_promode";
+ }
+ else if ( *iWeaponID == TF_WEAPON_BAT_WOOD )
+ {
+ killer_weapon_name = "deflect_ball";
+ }
+ else if ( *iWeaponID == TF_WEAPON_CANNON )
+ {
+ killer_weapon_name = "loose_cannon_reflect";
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if ( info.GetDamageCustom() == TF_DMG_CUSTOM_DEFENSIVE_STICKY )
+ {
+ killer_weapon_name = "sticky_resistance";
+ }
+
+ // strip certain prefixes from inflictor's classname
+ const char *prefix[] = { "tf_weapon_grenade_", "tf_weapon_", "NPC_", "func_" };
+ for ( int i = 0; i< ARRAYSIZE( prefix ); i++ )
+ {
+ // if prefix matches, advance the string pointer past the prefix
+ int len = Q_strlen( prefix[i] );
+ if ( strncmp( killer_weapon_name, prefix[i], len ) == 0 )
+ {
+ killer_weapon_name += len;
+ break;
+ }
+ }
+
+ // look out for sentry rocket as weapon and map it to sentry gun, so we get the sentry death icon based off level
+
+ if ( 0 == Q_strcmp( killer_weapon_name, "tf_projectile_sentryrocket" ) )
+ {
+ killer_weapon_name = "obj_sentrygun3";
+ }
+ else if ( 0 == Q_strcmp( killer_weapon_name, "obj_sentrygun" ) )
+ {
+ CObjectSentrygun *pSentrygun = assert_cast<CObjectSentrygun*>( pInflictor );
+ if ( pSentrygun )
+ {
+ if ( pSentrygun->IsMiniBuilding() )
+ {
+ killer_weapon_name = "obj_minisentry";
+ }
+ else
+ {
+ int iSentryLevel = pSentrygun->GetUpgradeLevel();
+ switch( iSentryLevel)
+ {
+ case 1:
+ killer_weapon_name = "obj_sentrygun";
+ break;
+ case 2:
+ killer_weapon_name = "obj_sentrygun2";
+ break;
+ case 3:
+ killer_weapon_name = "obj_sentrygun3";
+ break;
+ default:
+ killer_weapon_name = "obj_sentrygun";
+ break;
+ }
+ }
+ }
+ }
+ else if ( 0 == Q_strcmp( killer_weapon_name, "tf_projectile_healing_bolt" ) )
+ {
+ killer_weapon_name = "crusaders_crossbow";
+ }
+ else if ( 0 == Q_strcmp( killer_weapon_name, "tf_projectile_pipe" ) )
+ {
+ // let's look-up the primary weapon to see what type of grenade launcher it is
+ if ( pScorer )
+ {
+ CTFPlayer *pTFScorer = ToTFPlayer( pScorer );
+ if ( pTFScorer )
+ {
+ CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( pTFScorer->Weapon_GetWeaponByType( TF_WPN_TYPE_PRIMARY ) );
+ if ( pWeapon && ( pWeapon->GetWeaponID() == TF_WEAPON_GRENADELAUNCHER ) && pWeapon->GetAttributeContainer() )
+ {
+ CEconItemView *pItem = pWeapon->GetAttributeContainer()->GetItem();
+ if ( pItem && pItem->GetStaticData() )
+ {
+ if ( pItem->GetStaticData()->GetIconClassname() )
+ {
+ killer_weapon_name = pItem->GetStaticData()->GetIconClassname();
+ *iWeaponID = TF_WEAPON_NONE;
+ }
+ }
+ }
+ }
+ }
+ }
+ else if ( 0 == Q_strcmp( killer_weapon_name, "tf_projectile_energy_ring" ) )
+ {
+ CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>( info.GetWeapon() );
+ if ( pWeapon )
+ {
+ CEconItemView *pItem = pWeapon->GetAttributeContainer()->GetItem();
+ if ( pItem && pItem->GetStaticData() && pItem->GetStaticData()->GetIconClassname() )
+ {
+ killer_weapon_name = pItem->GetStaticData()->GetIconClassname();
+ *iWeaponID = TF_WEAPON_NONE;
+ }
+ }
+ }
+ else if ( 0 == Q_strcmp( killer_weapon_name, "obj_attachment_sapper" ) )
+ {
+ // let's look-up the sapper weapon to see what type it is
+ if ( pScorer )
+ {
+ CTFPlayer *pTFScorer = ToTFPlayer( pScorer );
+ if ( pTFScorer )
+ {
+ CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase *>( pTFScorer->GetEntityForLoadoutSlot( LOADOUT_POSITION_BUILDING ) );
+
+ if ( pWeapon && ( pWeapon->GetWeaponID() == TF_WEAPON_BUILDER ) && pWeapon->GetAttributeContainer() )
+ {
+ CEconItemView *pItem = pWeapon->GetAttributeContainer()->GetItem();
+ if ( pItem && pItem->GetStaticData() )
+ {
+ if ( pItem->GetStaticData()->GetIconClassname() )
+ {
+ killer_weapon_name = pItem->GetStaticData()->GetIconClassname();
+ *iWeaponID = TF_WEAPON_NONE;
+ }
+ }
+ }
+ }
+ }
+ }
+ else if ( ( info.GetDamageCustom() == TF_DMG_CUSTOM_STANDARD_STICKY ) || ( info.GetDamageCustom() == TF_DMG_CUSTOM_AIR_STICKY_BURST ) )
+ {
+ // let's look-up the secondary weapon to see what type of sticky launcher it is
+ if ( pScorer )
+ {
+ CTFPlayer *pTFScorer = ToTFPlayer( pScorer );
+ if ( pTFScorer )
+ {
+ CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( pTFScorer->Weapon_GetWeaponByType( TF_WPN_TYPE_SECONDARY ) );
+ if ( pWeapon && ( pWeapon->GetWeaponID() == TF_WEAPON_PIPEBOMBLAUNCHER ) && pWeapon->GetAttributeContainer() )
+ {
+ CEconItemView *pItem = pWeapon->GetAttributeContainer()->GetItem();
+ if ( pItem && pItem->GetStaticData() )
+ {
+ if ( pItem->GetStaticData()->GetIconClassname() )
+ {
+ killer_weapon_name = pItem->GetStaticData()->GetIconClassname();
+ *iWeaponID = TF_WEAPON_NONE;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return killer_weapon_name;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: returns the player who assisted in the kill, or NULL if no assister
+//-----------------------------------------------------------------------------
+CBasePlayer *CTFGameRules::GetAssister( CBasePlayer *pVictim, CBasePlayer *pScorer, CBaseEntity *pInflictor )
+{
+ CTFPlayer *pTFScorer = ToTFPlayer( pScorer );
+ CTFPlayer *pTFVictim = ToTFPlayer( pVictim );
+ if ( pTFScorer && pTFVictim )
+ {
+ // if victim killed himself, don't award an assist to anyone else, even if there was a recent damager
+ if ( pTFScorer == pTFVictim )
+ return NULL;
+
+ // If an assist has been specified already, use it.
+ if ( pTFVictim->m_Shared.GetAssist() )
+ {
+ return pTFVictim->m_Shared.GetAssist();
+ }
+
+ // If a player is healing the scorer, give that player credit for the assist
+ CTFPlayer *pHealer = ToTFPlayer( pTFScorer->m_Shared.GetFirstHealer() );
+ // Must be a medic to receive a healing assist, otherwise engineers get credit for assists from dispensers doing healing.
+ // Also don't give an assist for healing if the inflictor was a sentry gun, otherwise medics healing engineers get assists for the engineer's sentry kills.
+ if ( pHealer && ( TF_CLASS_MEDIC == pHealer->GetPlayerClass()->GetClassIndex() ) && ( NULL == dynamic_cast<CObjectSentrygun *>( pInflictor ) ) )
+ {
+ return pHealer;
+ }
+
+ // If we're under the effect of a condition that grants assists, give one to the player that buffed us
+ CTFPlayer *pCondAssister = ToTFPlayer( pTFScorer->m_Shared.GetConditionAssistFromAttacker() );
+ if ( pCondAssister )
+ return pCondAssister;
+
+ // See who has damaged the victim 2nd most recently (most recent is the killer), and if that is within a certain time window.
+ // If so, give that player an assist. (Only 1 assist granted, to single other most recent damager.)
+ CTFPlayer *pRecentDamager = GetRecentDamager( pTFVictim, 1, TF_TIME_ASSIST_KILL );
+ if ( pRecentDamager && ( pRecentDamager != pScorer ) )
+ return pRecentDamager;
+
+ // if a teammate has recently helped this sentry (ie: wrench hit), they assisted in the kill
+ CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( pInflictor );
+ if ( sentry )
+ {
+ CTFPlayer *pAssister = sentry->GetAssistingTeammate( TF_TIME_ASSIST_KILL );
+ if ( pAssister )
+ return pAssister;
+ }
+ }
+ return NULL;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Returns specified recent damager, if there is one who has done damage
+// within the specified time period. iDamager=0 returns the most recent
+// damager, iDamager=1 returns the next most recent damager.
+//-----------------------------------------------------------------------------
+CTFPlayer *CTFGameRules::GetRecentDamager( CTFPlayer *pVictim, int iDamager, float flMaxElapsed )
+{
+ EntityHistory_t *damagerHistory = pVictim->m_AchievementData.GetDamagerHistory( iDamager );
+ if ( !damagerHistory )
+ return NULL;
+
+ if ( damagerHistory->hEntity && ( gpGlobals->curtime - damagerHistory->flTimeDamage <= flMaxElapsed ) )
+ {
+ CTFPlayer *pRecentDamager = ToTFPlayer( damagerHistory->hEntity );
+ if ( pRecentDamager )
+ return pRecentDamager;
+ }
+ return NULL;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Returns who should be awarded the kill
+//-----------------------------------------------------------------------------
+CBasePlayer *CTFGameRules::GetDeathScorer( CBaseEntity *pKiller, CBaseEntity *pInflictor, CBaseEntity *pVictim )
+{
+ if ( ( pKiller == pVictim ) && ( pKiller == pInflictor ) )
+ {
+ // If this was an explicit suicide, see if there was a damager within a certain time window. If so, award this as a kill to the damager.
+ CTFPlayer *pTFVictim = ToTFPlayer( pVictim );
+ if ( pTFVictim )
+ {
+ CTFPlayer *pRecentDamager = GetRecentDamager( pTFVictim, 0, TF_TIME_SUICIDE_KILL_CREDIT );
+ if ( pRecentDamager )
+ return pRecentDamager;
+ }
+ }
+
+ //Handle Pyro's Deflection credit.
+ CTFWeaponBaseGrenadeProj *pBaseGrenade = dynamic_cast<CTFWeaponBaseGrenadeProj*>( pInflictor );
+
+ if ( pBaseGrenade )
+ {
+ if ( pBaseGrenade->GetDeflected() )
+ {
+ if ( pBaseGrenade->GetWeaponID() == TF_WEAPON_GRENADE_PIPEBOMB )
+ {
+ CTFPlayer *pDeflectOwner = ToTFPlayer( pBaseGrenade->GetDeflectOwner() );
+
+ if ( pDeflectOwner )
+ {
+ if ( pDeflectOwner->InSameTeam( pVictim ) == false )
+ return pDeflectOwner;
+ else
+ {
+ pBaseGrenade->ResetDeflected();
+ pBaseGrenade->SetDeflectOwner( NULL );
+ }
+ }
+ }
+ }
+ }
+
+ return BaseClass::GetDeathScorer( pKiller, pInflictor, pVictim );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+// Input : *pVictim -
+// *pKiller -
+// *pInflictor -
+//-----------------------------------------------------------------------------
+void CTFGameRules::DeathNotice( CBasePlayer *pVictim, const CTakeDamageInfo &info )
+{
+ DeathNotice( pVictim, info, "player_death" );
+}
+
+void CTFGameRules::DeathNotice( CBasePlayer *pVictim, const CTakeDamageInfo &info, const char* eventName )
+{
+ int killer_ID = 0;
+
+ // Find the killer & the scorer
+ CTFPlayer *pTFPlayerVictim = ToTFPlayer( pVictim );
+ CBaseEntity *pInflictor = info.GetInflictor();
+ CBaseEntity *pKiller = info.GetAttacker();
+ CTFPlayer *pScorer = ToTFPlayer( GetDeathScorer( pKiller, pInflictor, pVictim ) );
+ CTFPlayer *pAssister = ToTFPlayer( GetAssister( pVictim, pScorer, pInflictor ) );
+
+ // You can't assist yourself!
+ Assert( pScorer != pAssister || !pScorer );
+ if ( pScorer == pAssister && pScorer )
+ {
+ pAssister = NULL;
+ }
+
+ // Silent killers cause no death notices.
+ bool bSilentKill = false;
+ CTFPlayer *pAttacker = (CTFPlayer*)ToTFPlayer( info.GetAttacker() );
+ if ( pAttacker )
+ {
+ CTFWeaponBase *pWpn = pAttacker->GetActiveTFWeapon();
+ if ( pWpn && pWpn->IsSilentKiller() && ( info.GetDamageCustom() == TF_DMG_CUSTOM_BACKSTAB ) )
+ bSilentKill = true;
+ }
+
+ // Determine whether it's a feign death fake death notice
+ bool bFeignDeath = pTFPlayerVictim->IsGoingFeignDeath();
+ if ( bFeignDeath )
+ {
+ CTFPlayer *pDisguiseTarget = ToTFPlayer( pTFPlayerVictim->m_Shared.GetDisguiseTarget() );
+ if ( pDisguiseTarget && (pTFPlayerVictim->GetTeamNumber() == pDisguiseTarget->GetTeamNumber()) )
+ {
+ // We're disguised as a team mate. Pretend to die as that player instead of us.
+ pVictim = pTFPlayerVictim = pDisguiseTarget;
+ }
+ }
+
+ // Work out what killed the player, and send a message to all clients about it
+ int iWeaponID;
+ const char *killer_weapon_name = GetKillingWeaponName( info, pTFPlayerVictim, &iWeaponID );
+ const char *killer_weapon_log_name = killer_weapon_name;
+
+ // Kill eater events.
+ {
+ // Was this a sentry kill? If the sentry did the kill itself with bullets then it'll be the inflictor.
+ // If it got the kill by firing a rocket, the rocket will be the inflictor and the sentry will be the
+ // owner of the rocket.
+ //
+ // Holy crap dynamic_cast quagmire of sadness below.
+ CObjectSentrygun *pSentrygun = dynamic_cast<CObjectSentrygun *>( pInflictor );
+ if ( !pSentrygun )
+ {
+ pSentrygun = pInflictor ? dynamic_cast<CObjectSentrygun *>( pInflictor->GetOwnerEntity() ) : NULL;
+ }
+
+ if ( pSentrygun )
+ {
+ // Try to grab the wrench that the engineer has equipped right now. We destroy sentries when wrenches
+ // get changed so whatever they have equipped right now counts for the wrench that built this sentry.
+ CTFPlayer *pBuilder = pSentrygun->GetBuilder();
+ if ( pBuilder )
+ {
+ EconEntity_OnOwnerKillEaterEvent( dynamic_cast<CEconEntity *>( pBuilder->GetEntityForLoadoutSlot( LOADOUT_POSITION_MELEE ) ), pScorer, pTFPlayerVictim, kKillEaterEvent_PlayerKillsBySentry );
+ // PDA's Also count Sentry kills
+ EconEntity_OnOwnerKillEaterEvent( dynamic_cast<CEconEntity *>( pBuilder->GetEntityForLoadoutSlot( LOADOUT_POSITION_PDA ) ), pScorer, pTFPlayerVictim, kKillEaterEvent_PlayerKillsBySentry );
+
+ // Check if the engineer is using a Wrangler on this sentry
+ CTFLaserPointer* pLaserPointer = dynamic_cast< CTFLaserPointer * >( pBuilder->GetEntityForLoadoutSlot( LOADOUT_POSITION_SECONDARY ) );
+ if ( pLaserPointer && pLaserPointer->HasLaserDot() )
+ {
+ EconEntity_OnOwnerKillEaterEvent( dynamic_cast<CEconEntity *>( pLaserPointer ), pScorer, pTFPlayerVictim, kKillEaterEvent_PlayerKillsByManualControlOfSentry );
+ }
+ }
+ }
+
+ // Should we award an assist kill to someone?
+ if ( pAssister )
+ {
+ EconEntity_OnOwnerKillEaterEvent( dynamic_cast<CEconEntity *>( pAssister->GetActiveWeapon() ), pAssister, pTFPlayerVictim, kKillEaterEvent_PlayerKillAssist );
+ HatAndMiscEconEntities_OnOwnerKillEaterEvent( pAssister, pTFPlayerVictim, kKillEaterEvent_CosmeticAssists );
+ }
+ }
+
+ if ( pScorer ) // Is the killer a client?
+ {
+ killer_ID = pScorer->GetUserID();
+ }
+
+ CTFWeaponBase *pScorerWeapon = NULL;
+ if ( pScorer )
+ {
+ pScorerWeapon = dynamic_cast< CTFWeaponBase * >( pScorer->Weapon_OwnsThisID( iWeaponID ) );
+ if ( pScorerWeapon )
+ {
+ CEconItemView *pItem = pScorerWeapon->GetAttributeContainer()->GetItem();
+
+ if ( pItem )
+ {
+ if ( pItem->GetStaticData()->GetIconClassname() )
+ {
+ killer_weapon_name = pItem->GetStaticData()->GetIconClassname();
+ }
+
+ if ( pItem->GetStaticData()->GetLogClassname() )
+ {
+ killer_weapon_log_name = pItem->GetStaticData()->GetLogClassname();
+ }
+ }
+ }
+ }
+
+ // In Arena mode award first blood to the first player that kills an enemy.
+ bool bKillWasFirstBlood = false;
+ if ( IsFirstBloodAllowed() )
+ {
+ if ( pScorer && pVictim && pScorer != pVictim )
+ {
+ if ( !FStrEq( eventName, "fish_notice" ) && !FStrEq( eventName, "fish_notice__arm" ) && !FStrEq( eventName, "throwable_hit" ) )
+ {
+#ifndef _DEBUG
+ if ( GetGlobalTeam( pVictim->GetTeamNumber() ) && GetGlobalTeam( pVictim->GetTeamNumber() )->GetNumPlayers() > 1 )
+#endif // !DEBUG
+ {
+ float flFastTime = IsCompetitiveMode() ? 120.f : TF_ARENA_MODE_FAST_FIRST_BLOOD_TIME;
+ float flSlowTime = IsCompetitiveMode() ? 300.f : TF_ARENA_MODE_SLOW_FIRST_BLOOD_TIME;
+
+ if ( ( gpGlobals->curtime - m_flRoundStartTime ) <= flFastTime )
+ {
+ BroadcastSound( 255, "Announcer.AM_FirstBloodFast" );
+ }
+ else if ( ( gpGlobals->curtime - m_flRoundStartTime ) >= flSlowTime )
+ {
+ BroadcastSound( 255, "Announcer.AM_FirstBloodFinally" );
+ }
+ else
+ {
+ BroadcastSound( 255, "Announcer.AM_FirstBloodRandom" );
+ }
+
+ m_bArenaFirstBlood = true;
+ bKillWasFirstBlood = true;
+
+ if ( IsCompetitiveMode() )
+ {
+// CTF_GameStats.Event_PlayerAwardBonusPoints( pScorer, pVictim, 10 );
+//
+// CUtlVector< CTFPlayer* > vecPlayers;
+// CollectPlayers( &vecPlayers, pScorer->GetTeamNumber(), false );
+// FOR_EACH_VEC( vecPlayers, i )
+// {
+// if ( !vecPlayers[i] )
+// continue;
+//
+// if ( vecPlayers[i] == pScorer )
+// continue;
+//
+// CTF_GameStats.Event_PlayerAwardBonusPoints( vecPlayers[i], pVictim, 5 );
+// }
+ }
+ else
+ {
+ pScorer->m_Shared.AddCond( TF_COND_CRITBOOSTED_FIRST_BLOOD, TF_ARENA_MODE_FIRST_BLOOD_CRIT_TIME );
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ // so you can't turn on the ConVar in the middle of a round and get the first blood reward
+ m_bArenaFirstBlood = true;
+ }
+
+ // Awesome hack for pyroland silliness: if there was no other assister, and the person that got
+ // the kill has some sort of "pet" item (balloonicorn, brainslug, etc.), we send the name of
+ // that item down as the assister. We'll use a custom name if available and fall back to the
+ // localization token (localized on the client) if not.
+ CUtlConstString sAssisterOverrideDesc;
+
+ if ( pScorer && !pAssister )
+ {
+ // Find out whether the killer has at least one item that will ask for kill assist credit.
+ int iKillerHasPetItem = 0;
+ CUtlVector<CBaseEntity *> vecItems;
+ CALL_ATTRIB_HOOK_FLOAT_ON_OTHER_WITH_ITEMS( pScorer, iKillerHasPetItem, &vecItems, counts_as_assister );
+
+ FOR_EACH_VEC( vecItems, i )
+ {
+ CEconEntity *pEconEntity = dynamic_cast<CEconEntity *>( vecItems[i] );
+ if ( !pEconEntity )
+ continue;
+
+ CEconItemView *pEconItemView = pEconEntity->GetAttributeContainer()->GetItem();
+ if ( !pEconItemView )
+ continue;
+
+ if ( pEconItemView->GetCustomName() )
+ {
+ sAssisterOverrideDesc = CFmtStr( "%c%s", iKillerHasPetItem == 2 ? kHorriblePyroVisionHack_KillAssisterType_CustomName_First : kHorriblePyroVisionHack_KillAssisterType_CustomName, pEconItemView->GetCustomName() );
+ }
+ else
+ {
+ sAssisterOverrideDesc = CFmtStr( "%c%s", iKillerHasPetItem == 2 ? kHorriblePyroVisionHack_KillAssisterType_LocalizationString_First : kHorriblePyroVisionHack_KillAssisterType_LocalizationString, pEconItemView->GetItemDefinition()->GetItemBaseName() ).Get();
+ }
+ break;
+ }
+ }
+
+ IGameEvent * event = gameeventmanager->CreateEvent( eventName /* "player_death" */ );
+
+ //if ( event && FStrEq( eventName, "throwable_hit" ) )
+ //{
+ // int iHitCount = 1;
+ // PlayerStats_t *pStats = CTF_GameStats.FindPlayerStats( pScorer );
+ // if ( pStats )
+ // {
+ // iHitCount = pStats->statsAccumulated.Get( TFSTAT_THROWABLEHIT );
+ // }
+
+ // event->SetInt( "totalhits", iHitCount );
+ //}
+
+ if ( event )
+ {
+ event->SetInt( "userid", pVictim->GetUserID() );
+ event->SetInt( "victim_entindex", pVictim->entindex() );
+ event->SetInt( "attacker", killer_ID );
+ event->SetInt( "assister", pAssister ? pAssister->GetUserID() : -1 );
+ event->SetString( "weapon", killer_weapon_name );
+ event->SetString( "weapon_logclassname", killer_weapon_log_name );
+ event->SetInt( "weaponid", iWeaponID );
+ event->SetInt( "damagebits", info.GetDamageType() );
+ event->SetInt( "customkill", info.GetDamageCustom() );
+ event->SetInt( "inflictor_entindex", pInflictor ? pInflictor->entindex() : -1 );
+ event->SetInt( "priority", 7 ); // HLTV event priority, not transmitted
+
+ if ( info.GetPlayerPenetrationCount() > 0 )
+ {
+ event->SetInt( "playerpenetratecount", info.GetPlayerPenetrationCount() );
+ }
+
+ if ( !sAssisterOverrideDesc.IsEmpty() )
+ {
+ event->SetString( "assister_fallback", sAssisterOverrideDesc.Get() );
+ }
+
+ event->SetBool( "silent_kill", bSilentKill );
+
+ int iDeathFlags = pTFPlayerVictim->GetDeathFlags();
+
+ if ( bKillWasFirstBlood )
+ {
+ iDeathFlags |= TF_DEATH_FIRST_BLOOD;
+ }
+
+ if ( bFeignDeath )
+ {
+ iDeathFlags |= TF_DEATH_FEIGN_DEATH;
+ }
+
+ if ( pTFPlayerVictim->WasGibbedOnLastDeath() )
+ {
+ iDeathFlags |= TF_DEATH_GIBBED;
+ }
+
+ if ( pTFPlayerVictim->IsInPurgatory() )
+ {
+ iDeathFlags |= TF_DEATH_PURGATORY;
+ }
+
+ if ( pTFPlayerVictim->IsMiniBoss() )
+ {
+ iDeathFlags |= TF_DEATH_MINIBOSS;
+ }
+
+ // Australium Guns get a Gold Background
+ IHasAttributes *pAttribInterface = GetAttribInterface( info.GetWeapon() );
+ if ( pAttribInterface )
+ {
+ int iIsAustralium = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( info.GetWeapon(), iIsAustralium, is_australium_item );
+ if ( iIsAustralium )
+ {
+ iDeathFlags |= TF_DEATH_AUSTRALIUM;
+ }
+ }
+
+ // We call this directly since we need more information than provided in the event alone.
+ if ( FStrEq( eventName, "player_death" ) )
+ {
+ CTF_GameStats.Event_KillDetail( pScorer, pTFPlayerVictim, pAssister, event, info );
+ event->SetInt( "kill_streak_victim", pTFPlayerVictim->m_Shared.GetStreak( CTFPlayerShared::kTFStreak_Kills ) );
+ event->SetBool( "rocket_jump", ( pTFPlayerVictim->RocketJumped() == 1 ) );
+ event->SetInt( "crit_type", info.GetCritType() );
+
+ // Kill streak updating
+ if ( pTFPlayerVictim && pScorer && pTFPlayerVictim != pScorer )
+ {
+ // Propagate duckstreaks
+ event->SetInt( "duck_streak_victim", pTFPlayerVictim->m_Shared.GetStreak( CTFPlayerShared::kTFStreak_Ducks ) );
+ event->SetInt( "duck_streak_total", pScorer->m_Shared.GetStreak( CTFPlayerShared::kTFStreak_Ducks ) );
+ event->SetInt( "ducks_streaked", pScorer->m_Shared.GetLastDuckStreakIncrement() );
+
+ // Check if they have the appropriate attribute.
+ int iKillStreak = 0;
+ int iKills = 0;
+ CBaseEntity *pKillStreakTarget = NULL;
+ if ( !pAttribInterface )
+ {
+ // Check if you are a sentry and if so, use the wrench
+ // For Sentries Inflictor can be the sentry (bullets) or the Sentry Rocket
+
+ CObjectSentrygun *pSentry = dynamic_cast<CObjectSentrygun*>( pInflictor );
+ if ( !pSentry && pInflictor )
+ {
+ pSentry = dynamic_cast<CObjectSentrygun*>( pInflictor->GetOwnerEntity() );
+ }
+
+ if ( pSentry )
+ {
+ pKillStreakTarget = dynamic_cast<CTFWeaponBase*>( pScorer->GetEntityForLoadoutSlot( LOADOUT_POSITION_MELEE ) );
+ }
+ }
+ else
+ {
+ pKillStreakTarget = info.GetWeapon();
+ }
+
+ if ( pKillStreakTarget )
+ {
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pKillStreakTarget, iKillStreak, killstreak_tier );
+#ifdef STAGING_ONLY
+ if ( tf_killstreak_alwayson.GetBool() )
+ {
+ iKillStreak = 1;
+ }
+#endif
+ // Always track killstreak regardless of the attribute for data collection purposes
+ pScorer->m_Shared.IncrementStreak( CTFPlayerShared::kTFStreak_KillsAll, 1 );
+ if ( iKillStreak )
+ {
+ iKills = pScorer->m_Shared.IncrementStreak( CTFPlayerShared::kTFStreak_Kills, 1 );
+ event->SetInt( "kill_streak_total", iKills );
+
+ int iWepKills = 0;
+ CTFWeaponBase *pWeapon = dynamic_cast<CTFWeaponBase*>( pKillStreakTarget );
+ if ( pWeapon )
+ {
+ iWepKills = pWeapon->GetKillStreak() + 1;
+ pWeapon->SetKillStreak( iWepKills );
+ }
+ else
+ {
+ CTFWearable *pWearable = dynamic_cast<CTFWearable*>( pKillStreakTarget );
+ if ( pWearable )
+ {
+ iWepKills = pWearable->GetKillStreak() + 1;
+ pWearable->SetKillStreak( iWepKills );
+ }
+ }
+
+ event->SetInt( "kill_streak_wep", iWepKills );
+
+ // Track each player's max streak per-round
+ CTF_GameStats.Event_PlayerEarnedKillStreak( pScorer );
+ }
+ }
+
+ if ( pAssister )
+ {
+ event->SetInt( "duck_streak_assist", pAssister->m_Shared.GetStreak( CTFPlayerShared::kTFStreak_Ducks ) );
+
+ // Only allow assists for Mediguns
+ CTFWeaponBase *pAssisterWpn = pAssister->GetActiveTFWeapon();
+ if ( pAssisterWpn && pAssister->IsPlayerClass( TF_CLASS_MEDIC ) )
+ {
+ CWeaponMedigun *pMedigun = dynamic_cast<CWeaponMedigun*>( pAssisterWpn );
+ if ( pMedigun )
+ {
+ iKillStreak = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pAssisterWpn, iKillStreak, killstreak_tier );
+#ifdef STAGING_ONLY
+ if ( tf_killstreak_alwayson.GetBool() )
+ {
+ iKillStreak = 1;
+ }
+#endif
+ if ( iKillStreak )
+ {
+ iKills = pAssister->m_Shared.IncrementStreak( CTFPlayerShared::kTFStreak_Kills, 1 );
+ event->SetInt( "kill_streak_assist", iKills );
+
+ int iWepKills = pAssisterWpn->GetKillStreak() + 1;
+ pAssisterWpn->SetKillStreak( iWepKills );
+
+ // Track each player's max streak per-round
+ CTF_GameStats.Event_PlayerEarnedKillStreak( pAssister );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ event->SetInt( "death_flags", iDeathFlags );
+ event->SetInt( "stun_flags", pTFPlayerVictim->m_iOldStunFlags );
+
+ item_definition_index_t weaponDefIndex = INVALID_ITEM_DEF_INDEX;
+ if ( pScorerWeapon )
+ {
+ CEconItemView *pItem = pScorerWeapon->GetAttributeContainer()->GetItem();
+ if ( pItem )
+ {
+ weaponDefIndex = pItem->GetItemDefIndex();
+ }
+ }
+ else if ( pScorer && pScorer->GetActiveTFWeapon() )
+ {
+ // get from active weapon instead
+ CEconItemView *pItem = pScorer->GetActiveTFWeapon()->GetAttributeContainer()->GetItem();
+ if ( pItem )
+ {
+ weaponDefIndex = pItem->GetItemDefIndex();
+ }
+ }
+ event->SetInt( "weapon_def_index", weaponDefIndex );
+
+ pTFPlayerVictim->m_iOldStunFlags = 0;
+
+ gameeventmanager->FireEvent( event );
+ }
+}
+
+void CTFGameRules::ClientDisconnected( edict_t *pClient )
+{
+ CTFPlayer *pPlayer = ToTFPlayer( GetContainingEntity( pClient ) );
+ if ( pPlayer )
+ {
+ // ACHIEVEMENT_TF_PYRO_DOMINATE_LEAVESVR - Pyro causes a dominated player to leave the server
+ for ( int i = 1; i <= gpGlobals->maxClients ; i++ )
+ {
+ if ( pPlayer->m_Shared.IsPlayerDominatingMe(i) )
+ {
+ CTFPlayer *pDominatingPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( pDominatingPlayer && pDominatingPlayer != pPlayer )
+ {
+ if ( pDominatingPlayer->IsPlayerClass(TF_CLASS_PYRO) )
+ {
+ pDominatingPlayer->AwardAchievement( ACHIEVEMENT_TF_PYRO_DOMINATE_LEAVESVR );
+ }
+ }
+ }
+ }
+
+ CTFPlayerResource *pTFResource = dynamic_cast< CTFPlayerResource* >( g_pPlayerResource );
+ if ( pTFResource )
+ {
+ if ( pPlayer->entindex() == pTFResource->GetPartyLeaderIndex( pPlayer->GetTeamNumber() ) )
+ {
+ // the leader is leaving so reset the player resource index
+ pTFResource->SetPartyLeaderIndex( pPlayer->GetTeamNumber(), 0 );
+ }
+ }
+
+ // Notify gamestats that the player left.
+ CTF_GameStats.Event_PlayerDisconnectedTF( pPlayer );
+
+ // Check Ready status for the player
+ if ( UsePlayerReadyStatusMode() )
+ {
+ if ( !pPlayer->IsBot() && State_Get() != GR_STATE_RND_RUNNING )
+ {
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( !pMatchDesc || !pMatchDesc->m_params.m_bAutoReady )
+ {
+ // Always reset when a player leaves this type of match
+ PlayerReadyStatus_ResetState();
+ }
+ else if ( !IsTeamReady( pPlayer->GetTeamNumber() ) )
+ {
+ if ( IsPlayerReady( pPlayer->entindex() ) )
+ {
+ // Clear the ready status so it doesn't block the rest of the team
+ PlayerReadyStatus_UpdatePlayerState( pPlayer, false );
+ }
+ else
+ {
+ // Disconnecting player wasn't ready, but is the rest of the team?
+ // If so, we want to cancel the ready_state so it doesn't start right away when this player disconnects
+ bool bEveryoneReady = true;
+ CUtlVector<CTFPlayer *> playerVector;
+ CollectPlayers( &playerVector, pPlayer->GetTeamNumber() );
+ FOR_EACH_VEC( playerVector, i )
+ {
+ if ( !playerVector[i] )
+ continue;
+
+ if ( playerVector[i]->IsBot() )
+ continue;
+
+ if ( playerVector[i] == pPlayer )
+ continue;
+
+ if ( !IsPlayerReady( playerVector[i]->entindex() ) )
+ {
+ bEveryoneReady = false;
+ }
+ }
+
+ if ( bEveryoneReady )
+ {
+ PlayerReadyStatus_ResetState();
+ }
+ }
+ }
+ else
+ {
+ // If we're currently in a countdown we should cancel it
+ if ( ( m_flRestartRoundTime > 0 ) || ( mp_restartgame.GetInt() > 0 ) )
+ {
+ PlayerReadyStatus_ResetState();
+ }
+ }
+ }
+ }
+
+ // clean up anything they left behind
+ pPlayer->TeamFortress_ClientDisconnected();
+ Arena_ClientDisconnect( pPlayer->GetPlayerName() );
+ }
+
+ BaseClass::ClientDisconnected( pClient );
+
+ // are any of the spies disguising as this player?
+ for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ )
+ {
+ CTFPlayer *pTemp = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( pTemp && pTemp != pPlayer )
+ {
+ if ( pTemp->m_Shared.GetDisguiseTarget() == pPlayer )
+ {
+ // choose someone else...
+ pTemp->m_Shared.FindDisguiseTarget();
+ }
+ }
+ }
+}
+
+// Falling damage stuff.
+#define TF_PLAYER_MAX_SAFE_FALL_SPEED 650
+
+float CTFGameRules::FlPlayerFallDamage( CBasePlayer *pPlayer )
+{
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+ if ( !pTFPlayer )
+ return 0;
+
+ // Karts don't take fall damage
+ if ( pTFPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_KART ) )
+ {
+ return 0;
+ }
+
+ // grappling hook don't take fall damage
+ if ( pTFPlayer->GetGrapplingHookTarget() )
+ {
+ return 0;
+ }
+
+ if ( pTFPlayer->m_Shared.GetCarryingRuneType() == RUNE_AGILITY )
+ {
+ return 0;
+ }
+
+ if ( pPlayer->m_Local.m_flFallVelocity > TF_PLAYER_MAX_SAFE_FALL_SPEED )
+ {
+ // Old TFC damage formula
+ float flFallDamage = 5 * (pPlayer->m_Local.m_flFallVelocity / 300);
+
+ // Fall damage needs to scale according to the player's max health, or
+ // it's always going to be much more dangerous to weaker classes than larger.
+ float flRatio = (float)pPlayer->GetMaxHealth() / 100.0;
+ flFallDamage *= flRatio;
+
+ flFallDamage *= random->RandomFloat( 0.8, 1.2 );
+
+ int iCancelFallingDamage = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pPlayer, iCancelFallingDamage, cancel_falling_damage );
+ if ( iCancelFallingDamage > 0 )
+ flFallDamage = 0;
+
+ return flFallDamage;
+ }
+
+ // Fall caused no damage
+ return 0;
+}
+
+void CTFGameRules::SendArenaWinPanelInfo( void )
+{
+ IGameEvent *winEvent = gameeventmanager->CreateEvent( "arena_win_panel" );
+
+ if ( winEvent )
+ {
+ int iBlueScore = GetGlobalTeam( TF_TEAM_BLUE )->GetScore();
+ int iRedScore = GetGlobalTeam( TF_TEAM_RED )->GetScore();
+ int iBlueScorePrev = iBlueScore;
+ int iRedScorePrev = iRedScore;
+
+ // if this is a complete round, calc team scores prior to this win
+ switch ( m_iWinningTeam )
+ {
+ case TF_TEAM_BLUE:
+ {
+ iBlueScorePrev = ( iBlueScore - TEAMPLAY_ROUND_WIN_SCORE >= 0 ) ? ( iBlueScore - TEAMPLAY_ROUND_WIN_SCORE ) : 0;
+
+ if ( IsInTournamentMode() == false )
+ {
+ iRedScore = 0;
+ }
+ }
+ break;
+
+ case TF_TEAM_RED:
+ {
+ iRedScorePrev = ( iRedScore - TEAMPLAY_ROUND_WIN_SCORE >= 0 ) ? ( iRedScore - TEAMPLAY_ROUND_WIN_SCORE ) : 0;
+
+ if ( IsInTournamentMode() == false )
+ {
+ iBlueScore = 0;
+ }
+
+ break;
+ }
+ case TEAM_UNASSIGNED:
+ break; // stalemate; nothing to do
+ }
+
+ winEvent->SetInt( "panel_style", WINPANEL_BASIC );
+ winEvent->SetInt( "winning_team", m_iWinningTeam );
+ winEvent->SetInt( "winreason", m_iWinReason );
+ winEvent->SetString( "cappers", ( m_iWinReason == WINREASON_ALL_POINTS_CAPTURED || m_iWinReason == WINREASON_FLAG_CAPTURE_LIMIT ) ? m_szMostRecentCappers : "" );
+ winEvent->SetInt( "blue_score", iBlueScore );
+ winEvent->SetInt( "red_score", iRedScore );
+ winEvent->SetInt( "blue_score_prev", iBlueScorePrev );
+ winEvent->SetInt( "red_score_prev", iRedScorePrev );
+
+ CTFPlayerResource *pResource = dynamic_cast< CTFPlayerResource * >( g_pPlayerResource );
+ if ( !pResource )
+ return;
+
+ // build a vector of players & round scores
+ CUtlVector<PlayerArenaRoundScore_t> vecPlayerScore;
+ int iPlayerIndex;
+ for( iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+ if ( !pTFPlayer || !pTFPlayer->IsConnected() )
+ continue;
+ // filter out spectators and, if not stalemate, all players not on winning team
+ int iPlayerTeam = pTFPlayer->GetTeamNumber();
+ if ( ( iPlayerTeam < FIRST_GAME_TEAM ) )
+ continue;
+
+ PlayerStats_t *pStats = CTF_GameStats.FindPlayerStats( pTFPlayer );
+ PlayerArenaRoundScore_t &playerRoundScore = vecPlayerScore[vecPlayerScore.AddToTail()];
+
+ playerRoundScore.iPlayerIndex = iPlayerIndex;
+
+ if ( pStats )
+ {
+ playerRoundScore.iTotalDamage = pStats->statsCurrentRound.m_iStat[TFSTAT_DAMAGE];
+ playerRoundScore.iTotalHealing = pStats->statsCurrentRound.m_iStat[TFSTAT_HEALING];
+
+ if ( pTFPlayer->IsAlive() == true )
+ {
+ playerRoundScore.iTimeAlive = (int)gpGlobals->curtime - pTFPlayer->GetSpawnTime();
+ }
+ else
+ {
+ playerRoundScore.iTimeAlive = (int)pTFPlayer->GetDeathTime() - pTFPlayer->GetSpawnTime();
+ }
+
+ playerRoundScore.iKillingBlows = pStats->statsCurrentRound.m_iStat[TFSTAT_KILLS];
+ playerRoundScore.iScore = CalcPlayerScore( &pStats->statsCurrentRound, pTFPlayer );
+ }
+ }
+
+ // sort the players by round score
+ vecPlayerScore.Sort( PlayerArenaRoundScoreSortFunc );
+
+ // set the top (up to) 6 players by round score in the event data
+ int numPlayers = 6;
+ int iPlayersAdded = 0;
+
+ // Add winners first
+ for ( int i = 0; i < vecPlayerScore.Count(); i++ )
+ {
+ if ( GetWinningTeam() == TEAM_UNASSIGNED )
+ {
+ if ( iPlayersAdded >= 6 )
+ break;
+ }
+ else
+ {
+ if ( iPlayersAdded >= 3 )
+ break;
+ }
+
+ int iPlayerIndex = vecPlayerScore[i].iPlayerIndex;
+
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+
+ if ( pTFPlayer && pTFPlayer->GetTeamNumber() != GetWinningTeam() && GetWinningTeam() != TEAM_UNASSIGNED )
+ continue;
+
+ winEvent->SetInt( UTIL_VarArgs( "player_%d", iPlayersAdded + 1 ), vecPlayerScore[i].iPlayerIndex );
+ winEvent->SetInt( UTIL_VarArgs( "player_%d_damage", iPlayersAdded + 1 ), vecPlayerScore[i].iTotalDamage );
+ winEvent->SetInt( UTIL_VarArgs( "player_%d_healing", iPlayersAdded + 1 ), vecPlayerScore[i].iTotalHealing );
+ winEvent->SetInt( UTIL_VarArgs( "player_%d_lifetime", iPlayersAdded + 1 ), vecPlayerScore[i].iTimeAlive );
+ winEvent->SetInt( UTIL_VarArgs( "player_%d_kills", iPlayersAdded + 1 ), vecPlayerScore[i].iKillingBlows );
+
+ iPlayersAdded++;
+ }
+
+ if ( GetWinningTeam() != TEAM_UNASSIGNED )
+ {
+ //Now add the rest
+ iPlayersAdded = 3;
+
+ for ( int i = 0; i < vecPlayerScore.Count(); i++ )
+ {
+ if ( iPlayersAdded >= numPlayers )
+ break;
+
+ int iIndex = iPlayersAdded + 1;
+
+ int iPlayerIndex = vecPlayerScore[i].iPlayerIndex;
+
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+
+ if ( pTFPlayer && pTFPlayer->GetTeamNumber() == GetWinningTeam() )
+ continue;
+
+ winEvent->SetInt( UTIL_VarArgs( "player_%d", iIndex ), vecPlayerScore[i].iPlayerIndex );
+ winEvent->SetInt( UTIL_VarArgs( "player_%d_damage", iIndex ), vecPlayerScore[i].iTotalDamage );
+ winEvent->SetInt( UTIL_VarArgs( "player_%d_healing", iIndex ), vecPlayerScore[i].iTotalHealing );
+ winEvent->SetInt( UTIL_VarArgs( "player_%d_lifetime", iIndex ), vecPlayerScore[i].iTimeAlive );
+ winEvent->SetInt( UTIL_VarArgs( "player_%d_kills", iIndex ), vecPlayerScore[i].iKillingBlows );
+
+ iPlayersAdded++;
+ }
+ }
+
+ // Send the event
+ gameeventmanager->FireEvent( winEvent );
+ }
+}
+
+void CTFGameRules::SendPVEWinPanelInfo( void )
+{
+ IGameEvent *winEvent = gameeventmanager->CreateEvent( "pve_win_panel" );
+
+ if ( winEvent )
+ {
+ winEvent->SetInt( "panel_style", WINPANEL_BASIC );
+ winEvent->SetInt( "winning_team", m_iWinningTeam );
+ winEvent->SetInt( "winreason", 0 );
+
+ // Send the event
+ gameeventmanager->FireEvent( winEvent );
+ }
+
+ /*CBroadcastRecipientFilter filter;
+ filter.MakeReliable();
+ UserMessageBegin( filter, "MVMAnnouncement" );
+ WRITE_CHAR( TF_MVM_ANNOUNCEMENT_WAVE_FAILED );
+ WRITE_CHAR( -1 );
+ MessageEnd();*/
+}
+
+void CTFGameRules::SendWinPanelInfo( bool bGameOver )
+{
+ if ( IsInArenaMode() == true )
+ {
+ SendArenaWinPanelInfo();
+ return;
+ }
+
+ if ( IsPVEModeActive() )
+ {
+ SendPVEWinPanelInfo();
+ return;
+ }
+
+ IGameEvent *winEvent = gameeventmanager->CreateEvent( "teamplay_win_panel" );
+
+ if ( winEvent )
+ {
+ int iBlueScore = GetGlobalTeam( TF_TEAM_BLUE )->GetScore();
+ int iRedScore = GetGlobalTeam( TF_TEAM_RED )->GetScore();
+ int iBlueScorePrev = iBlueScore;
+ int iRedScorePrev = iRedScore;
+
+ bool bRoundComplete = m_bForceMapReset || ( IsGameUnderTimeLimit() && ( GetTimeLeft() <= 0 ) );
+
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ bool bScoringPerCapture = ( pMaster ) ? ( pMaster->ShouldScorePerCapture() ) : false;
+
+ if ( m_nGameType == TF_GAMETYPE_CTF )
+ {
+ if ( tf_flag_caps_per_round.GetInt() == 0 )
+ {
+ bScoringPerCapture = true;
+ }
+ }
+
+ if ( bRoundComplete && !bScoringPerCapture && m_bUseAddScoreAnim )
+ {
+ // if this is a complete round, calc team scores prior to this win
+ switch ( m_iWinningTeam )
+ {
+ case TF_TEAM_BLUE:
+ iBlueScorePrev = ( iBlueScore - TEAMPLAY_ROUND_WIN_SCORE >= 0 ) ? ( iBlueScore - TEAMPLAY_ROUND_WIN_SCORE ) : 0;
+ break;
+ case TF_TEAM_RED:
+ iRedScorePrev = ( iRedScore - TEAMPLAY_ROUND_WIN_SCORE >= 0 ) ? ( iRedScore - TEAMPLAY_ROUND_WIN_SCORE ) : 0;
+ break;
+ case TEAM_UNASSIGNED:
+ break; // stalemate; nothing to do
+ }
+ }
+
+ winEvent->SetInt( "panel_style", WINPANEL_BASIC );
+ winEvent->SetInt( "winning_team", m_iWinningTeam );
+ winEvent->SetInt( "winreason", m_iWinReason );
+ winEvent->SetString( "cappers", ( m_iWinReason == WINREASON_ALL_POINTS_CAPTURED || m_iWinReason == WINREASON_FLAG_CAPTURE_LIMIT ) ?
+ m_szMostRecentCappers : "" );
+ winEvent->SetInt( "flagcaplimit", IsPasstimeMode() ? tf_passtime_scores_per_round.GetInt() : tf_flag_caps_per_round.GetInt() );
+ winEvent->SetInt( "blue_score", iBlueScore );
+ winEvent->SetInt( "red_score", iRedScore );
+ winEvent->SetInt( "blue_score_prev", iBlueScorePrev );
+ winEvent->SetInt( "red_score_prev", iRedScorePrev );
+ winEvent->SetInt( "round_complete", bRoundComplete );
+
+ CTFPlayerResource *pResource = dynamic_cast< CTFPlayerResource * >( g_pPlayerResource );
+ if ( !pResource )
+ return;
+
+ // Highest killstreak
+ int nMaxStreakPlayerIndex = 0;
+ int nMaxStreakCount = 0;
+
+ // determine the 3 players on winning team who scored the most points this round
+
+ // build a vector of players & round scores
+ CUtlVector<PlayerRoundScore_t> vecPlayerScore;
+ int iPlayerIndex;
+ for( iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+ if ( !pTFPlayer || !pTFPlayer->IsConnected() )
+ continue;
+ // filter out spectators and, if not stalemate, all players not on winning team
+ int iPlayerTeam = pTFPlayer->GetTeamNumber();
+ if ( ( iPlayerTeam < FIRST_GAME_TEAM ) || ( m_iWinningTeam != TEAM_UNASSIGNED && ( m_iWinningTeam != iPlayerTeam ) ) )
+ continue;
+
+ int iRoundScore = 0, iTotalScore = 0;
+ PlayerStats_t *pStats = CTF_GameStats.FindPlayerStats( pTFPlayer );
+ if ( pStats )
+ {
+ iRoundScore = CalcPlayerScore( &pStats->statsCurrentRound, pTFPlayer );
+ iTotalScore = CalcPlayerScore( &pStats->statsAccumulated, pTFPlayer );
+ }
+
+ PlayerRoundScore_t &playerRoundScore = vecPlayerScore[vecPlayerScore.AddToTail()];
+
+ playerRoundScore.iRoundScore = iRoundScore;
+ playerRoundScore.iPlayerIndex = iPlayerIndex;
+ playerRoundScore.iTotalScore = iTotalScore;
+
+ // Highest killstreak?
+ PlayerStats_t *pPlayerStats = CTF_GameStats.FindPlayerStats( pTFPlayer );
+ if ( pPlayerStats )
+ {
+ int nMax = pPlayerStats->statsCurrentRound.m_iStat[TFSTAT_KILLSTREAK_MAX];
+ if ( nMax > nMaxStreakCount )
+ {
+ nMaxStreakCount = nMax;
+ nMaxStreakPlayerIndex = iPlayerIndex;
+ }
+ }
+ }
+
+ // sort the players by round score
+ vecPlayerScore.Sort( PlayerRoundScoreSortFunc );
+
+ // set the top (up to) 6 players by round score in the event data
+ int numPlayers = MIN( 6, vecPlayerScore.Count() );
+ for ( int i = 0; i < numPlayers; i++ )
+ {
+ // only include players who have non-zero points this round; if we get to a player with 0 round points, stop
+ if ( 0 == vecPlayerScore[i].iRoundScore )
+ break;
+
+ // set the player index and their round score in the event
+ char szPlayerIndexVal[64]="", szPlayerScoreVal[64]="";
+ Q_snprintf( szPlayerIndexVal, ARRAYSIZE( szPlayerIndexVal ), "player_%d", i+ 1 );
+ Q_snprintf( szPlayerScoreVal, ARRAYSIZE( szPlayerScoreVal ), "player_%d_points", i+ 1 );
+ winEvent->SetInt( szPlayerIndexVal, vecPlayerScore[i].iPlayerIndex );
+ winEvent->SetInt( szPlayerScoreVal, vecPlayerScore[i].iRoundScore );
+ }
+
+ winEvent->SetInt( "killstreak_player_1", nMaxStreakPlayerIndex );
+ winEvent->SetInt( "killstreak_player_1_count", nMaxStreakCount );
+
+#ifdef TF_RAID_MODE
+ if ( !bRoundComplete && ( TEAM_UNASSIGNED != m_iWinningTeam ) && !IsRaidMode() )
+#else
+ if ( !bRoundComplete && ( TEAM_UNASSIGNED != m_iWinningTeam ) )
+#endif // TF_RAID_MODE
+ {
+ // is this our new payload race game mode?
+ if ( ( m_nGameType == TF_GAMETYPE_ESCORT ) && ( m_bMultipleTrains == true ) )
+ {
+ if ( g_hControlPointMasters.Count() && g_hControlPointMasters[0] && g_hControlPointMasters[0]->PlayingMiniRounds() )
+ {
+ int nRoundsRemaining = g_hControlPointMasters[0]->NumPlayableControlPointRounds();
+ if ( nRoundsRemaining > 0 )
+ {
+ winEvent->SetInt( "rounds_remaining", nRoundsRemaining );
+ }
+ }
+ }
+ else
+ {
+ // if this was not a full round ending, include how many mini-rounds remain for winning team to win
+ if ( g_hControlPointMasters.Count() && g_hControlPointMasters[0] )
+ {
+ winEvent->SetInt( "rounds_remaining", g_hControlPointMasters[0]->CalcNumRoundsRemaining( m_iWinningTeam ) );
+ }
+ }
+ }
+
+ winEvent->SetBool( "game_over", bGameOver );
+
+ // Send the event
+ gameeventmanager->FireEvent( winEvent );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Sorts players by round score
+//-----------------------------------------------------------------------------
+int CTFGameRules::PlayerRoundScoreSortFunc( const PlayerRoundScore_t *pRoundScore1, const PlayerRoundScore_t *pRoundScore2 )
+{
+ // sort first by round score
+ if ( pRoundScore1->iRoundScore != pRoundScore2->iRoundScore )
+ return pRoundScore2->iRoundScore - pRoundScore1->iRoundScore;
+
+ // if round scores are the same, sort next by total score
+ if ( pRoundScore1->iTotalScore != pRoundScore2->iTotalScore )
+ return pRoundScore2->iTotalScore - pRoundScore1->iTotalScore;
+
+ // if scores are the same, sort next by player index so we get deterministic sorting
+ return ( pRoundScore2->iPlayerIndex - pRoundScore1->iPlayerIndex );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Sorts players by arena stats
+//-----------------------------------------------------------------------------
+int CTFGameRules::PlayerArenaRoundScoreSortFunc( const PlayerArenaRoundScore_t *pRoundScore1, const PlayerArenaRoundScore_t *pRoundScore2 )
+{
+ //Compare Total points first
+ //This gives us a rough estimate of performance
+ if ( pRoundScore1->iScore != pRoundScore2->iScore )
+ return pRoundScore2->iScore - pRoundScore1->iScore;
+
+ //Compare healing
+ if ( pRoundScore1->iTotalHealing != pRoundScore2->iTotalHealing )
+ return pRoundScore2->iTotalHealing - pRoundScore1->iTotalHealing;
+
+ //Compare damage
+ if ( pRoundScore1->iTotalDamage != pRoundScore2->iTotalDamage )
+ return pRoundScore2->iTotalDamage - pRoundScore1->iTotalDamage;
+
+ //Compare time alive
+ if ( pRoundScore1->iTimeAlive != pRoundScore2->iTimeAlive )
+ return pRoundScore2->iTimeAlive - pRoundScore1->iTimeAlive;
+
+ //Compare killing blows
+ if ( pRoundScore1->iKillingBlows != pRoundScore2->iKillingBlows )
+ return pRoundScore2->iKillingBlows - pRoundScore1->iKillingBlows;
+
+ // if scores are the same, sort next by player index so we get deterministic sorting
+ return ( pRoundScore2->iPlayerIndex - pRoundScore1->iPlayerIndex );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Called when the teamplay_round_win event is about to be sent, gives
+// this method a chance to add more data to it
+//-----------------------------------------------------------------------------
+void CTFGameRules::FillOutTeamplayRoundWinEvent( IGameEvent *event )
+{
+ event->SetInt( "flagcaplimit", IsPasstimeMode() ? tf_passtime_scores_per_round.GetInt() : tf_flag_caps_per_round.GetInt() );
+
+ // determine the losing team
+ int iLosingTeam;
+
+ switch( event->GetInt( "team" ) )
+ {
+ case TF_TEAM_RED:
+ iLosingTeam = TF_TEAM_BLUE;
+ break;
+ case TF_TEAM_BLUE:
+ iLosingTeam = TF_TEAM_RED;
+ break;
+ case TEAM_UNASSIGNED:
+ default:
+ iLosingTeam = TEAM_UNASSIGNED;
+ break;
+ }
+
+ // set the number of caps that team got any time during the round
+ event->SetInt( "losing_team_num_caps", m_iNumCaps[iLosingTeam] );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetupSpawnPointsForRound( void )
+{
+ if ( !g_hControlPointMasters.Count() || !g_hControlPointMasters[0] || !g_hControlPointMasters[0]->PlayingMiniRounds() )
+ return;
+
+ CTeamControlPointRound *pCurrentRound = g_hControlPointMasters[0]->GetCurrentRound();
+ if ( !pCurrentRound )
+ {
+ return;
+ }
+
+ // loop through the spawn points in the map and find which ones are associated with this round or the control points in this round
+ for ( int i=0; i<ITFTeamSpawnAutoList::AutoList().Count(); ++i )
+ {
+ CTFTeamSpawn *pTFSpawn = static_cast< CTFTeamSpawn* >( ITFTeamSpawnAutoList::AutoList()[i] );
+
+ CHandle<CTeamControlPoint> hControlPoint = pTFSpawn->GetControlPoint();
+ CHandle<CTeamControlPointRound> hRoundBlue = pTFSpawn->GetRoundBlueSpawn();
+ CHandle<CTeamControlPointRound> hRoundRed = pTFSpawn->GetRoundRedSpawn();
+
+ if ( hControlPoint && pCurrentRound->IsControlPointInRound( hControlPoint ) )
+ {
+ // this spawn is associated with one of our control points
+ pTFSpawn->SetDisabled( false );
+ pTFSpawn->ChangeTeam( hControlPoint->GetOwner() );
+ }
+ else if ( hRoundBlue && ( hRoundBlue == pCurrentRound ) )
+ {
+ pTFSpawn->SetDisabled( false );
+ pTFSpawn->ChangeTeam( TF_TEAM_BLUE );
+ }
+ else if ( hRoundRed && ( hRoundRed == pCurrentRound ) )
+ {
+ pTFSpawn->SetDisabled( false );
+ pTFSpawn->ChangeTeam( TF_TEAM_RED );
+ }
+ else
+ {
+ // this spawn isn't associated with this round or the control points in this round
+ pTFSpawn->SetDisabled( true );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CTFGameRules::SetCurrentRoundStateBitString( void )
+{
+ m_iPrevRoundState = m_iCurrentRoundState;
+
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+
+ if ( !pMaster )
+ {
+ return 0;
+ }
+
+ int iState = 0;
+
+ for ( int i=0; i<pMaster->GetNumPoints(); i++ )
+ {
+ CTeamControlPoint *pPoint = pMaster->GetControlPoint( i );
+
+ if ( pPoint->GetOwner() == TF_TEAM_BLUE )
+ {
+ // Set index to 1 for the point being owned by blue
+ iState |= ( 1<<i );
+ }
+ }
+
+ m_iCurrentRoundState = iState;
+
+ return iState;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetMiniRoundBitMask( int iMask )
+{
+ m_iCurrentMiniRoundMask = iMask;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsFirstBloodAllowed( void )
+{
+ // Already granted
+ if ( m_bArenaFirstBlood )
+ return false;
+
+ if ( IsInArenaMode() && tf_arena_first_blood.GetBool() )
+ return true;
+
+ if ( IsCompetitiveMode() && ( State_Get() == GR_STATE_RND_RUNNING ) )
+ {
+ if ( IsMatchTypeCompetitive() )
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: NULL pPlayer means show the panel to everyone
+//-----------------------------------------------------------------------------
+void CTFGameRules::ShowRoundInfoPanel( CTFPlayer *pPlayer /* = NULL */ )
+{
+ KeyValues *data = new KeyValues( "data" );
+
+ if ( m_iCurrentRoundState < 0 )
+ {
+ // Haven't set up the round state yet
+ return;
+ }
+
+ // if prev and cur are equal, we are starting from a fresh round
+ if ( m_iPrevRoundState >= 0 && pPlayer == NULL ) // we have data about a previous state
+ {
+ data->SetInt( "prev", m_iPrevRoundState );
+ }
+ else
+ {
+ // don't send a delta if this is just to one player, they are joining mid-round
+ data->SetInt( "prev", m_iCurrentRoundState );
+ }
+
+ data->SetInt( "cur", m_iCurrentRoundState );
+
+ // get bitmask representing the current miniround
+ data->SetInt( "round", m_iCurrentMiniRoundMask );
+
+ if ( pPlayer )
+ {
+ pPlayer->ShowViewPortPanel( PANEL_ROUNDINFO, true, data );
+ }
+ else
+ {
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( pTFPlayer && pTFPlayer->IsReadyToPlay() )
+ {
+ pTFPlayer->ShowViewPortPanel( PANEL_ROUNDINFO, true, data );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::TimerMayExpire( void )
+{
+ // Prevent timers expiring while control points are contested
+ int iNumControlPoints = ObjectiveResource()->GetNumControlPoints();
+ for ( int iPoint = 0; iPoint < iNumControlPoints; iPoint ++ )
+ {
+ if ( ObjectiveResource()->GetCappingTeam(iPoint) )
+ {
+ m_flCapInProgressBuffer = gpGlobals->curtime + 0.1f;
+ return false;
+ }
+ }
+
+ // This delay prevents an order-of-operations issue with caps that
+ // fire an AddTime output, and round timers' OnFinished output
+ if ( m_flCapInProgressBuffer >= gpGlobals->curtime )
+ return false;
+
+ if ( GetOvertimeAllowedForCTF() )
+ {
+ // Prevent timers expiring while flags are stolen/dropped
+ for ( int i=0; i<ICaptureFlagAutoList::AutoList().Count(); ++i )
+ {
+ CCaptureFlag *pFlag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[i] );
+ if ( !pFlag->IsDisabled() && !pFlag->IsHome() )
+ return false;
+ }
+ }
+
+ for ( int i = 0 ; i < m_CPTimerEnts.Count() ; i++ )
+ {
+ CCPTimerLogic *pTimer = m_CPTimerEnts[i];
+ if ( pTimer )
+ {
+ if ( pTimer->TimerMayExpire() == false )
+ return false;
+ }
+ }
+
+#ifdef TF_RAID_MODE
+ if ( IsRaidMode() && IsBossBattleMode() && tf_raid_allow_overtime.GetBool() )
+ {
+ CUtlVector< CTFPlayer * > alivePlayerVector;
+ CollectPlayers( &alivePlayerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS );
+
+ // if anyone is alive, go into overtime
+ if ( alivePlayerVector.Count() > 0 )
+ {
+ return false;
+ }
+ }
+#endif
+
+ return BaseClass::TimerMayExpire();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::BHavePlayers( void )
+{
+ CMatchInfo *pInfo = GTFGCClientSystem()->GetMatch();
+ if ( pInfo )
+ {
+ // When we have a match, we start reporting we're active as soon as the first person loads, and never stop.
+ return pInfo->m_bFirstPersonActive;
+ }
+
+ if ( IsInArenaMode() )
+ {
+ // At least two in queue, we're always able to play
+ if ( m_hArenaPlayerQueue.Count() >= 2 )
+ return true;
+
+ // Otherwise, return false if nobody is actually on a team, regardless of players ready-to-play state.
+ if ( GetGlobalTFTeam( TF_TEAM_BLUE )->GetNumPlayers() == 0 || GetGlobalTFTeam( TF_TEAM_RED )->GetNumPlayers() == 0 )
+ return false;
+
+ // Otherwise, fall through to base logic (e.g. 1v0 but already running)
+ }
+
+ return BaseClass::BHavePlayers();
+}
+
+int SortPlayerSpectatorQueue( CTFPlayer* const *p1, CTFPlayer* const *p2 )
+{
+ float flTime1 = (*p1)->GetTeamJoinTime();
+ float flTime2 = (*p2)->GetTeamJoinTime();
+
+ if ( flTime1 == flTime2 )
+ {
+ flTime1 = (*p1)->GetConnectionTime();
+ flTime2 = (*p2)->GetConnectionTime();
+
+ if ( flTime1 < flTime2 )
+ return -1;
+ }
+ else
+ {
+ if ( flTime1 > flTime2 )
+ return -1;
+ }
+
+ if ( flTime1 == flTime2 )
+ return 0;
+
+ return 1;
+}
+
+int SortPlayersScoreBased( CTFPlayer* const *p1, CTFPlayer* const *p2 )
+{
+ CTFPlayerResource *pResource = dynamic_cast<CTFPlayerResource *>( g_pPlayerResource );
+
+ if ( pResource )
+ {
+ int nScore2 = pResource->GetTotalScore( ( *p2 )->entindex() );
+ int nScore1 = pResource->GetTotalScore( ( *p1 )->entindex() );
+
+ // check the priority
+ if ( nScore2 > nScore1 )
+ {
+ return 1;
+ }
+ }
+
+ return -1;
+}
+
+// sort function for the list of players that we're going to use to scramble the teams
+int ScramblePlayersSort( CTFPlayer* const *p1, CTFPlayer* const *p2 )
+{
+ CTFPlayerResource *pResource = dynamic_cast< CTFPlayerResource * >( g_pPlayerResource );
+
+ if ( pResource )
+ {
+ int nScore2 = pResource->GetTotalScore( (*p2)->entindex() );
+ int nScore1 = pResource->GetTotalScore( (*p1)->entindex() );
+
+ // check the priority
+ if ( nScore2 > nScore1 )
+ {
+ return 1;
+ }
+ }
+
+ return -1;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::Arena_ClientDisconnect( const char *playername )
+{
+ if ( IsInArenaMode() == false )
+ return;
+
+ if ( IsInWaitingForPlayers() == true )
+ return;
+
+ if ( m_iRoundState != GR_STATE_PREROUND )
+ return;
+
+ if ( IsInTournamentMode() == true )
+ return;
+
+ int iLight, iHeavy;
+
+ if ( AreTeamsUnbalanced( iHeavy, iLight ) == true )
+ {
+ CTeam *pTeamHeavy = GetGlobalTeam( iHeavy );
+ CTeam *pTeamLight = GetGlobalTeam( iLight );
+
+ if ( pTeamHeavy == NULL || pTeamLight == NULL )
+ return;
+
+ int iPlayersNeeded = pTeamHeavy->GetNumPlayers() - pTeamLight->GetNumPlayers();
+
+ if ( m_hArenaPlayerQueue.Count() == 0 )
+ return;
+
+ for ( int iPlayers = 0; iPlayers < iPlayersNeeded; iPlayers++ )
+ {
+ CTFPlayer *pPlayer = m_hArenaPlayerQueue[iPlayers];
+
+ if ( pPlayer && pPlayer->IsHLTV() == false && pPlayer->IsReplay() == false )
+ {
+ pPlayer->ForceChangeTeam( TF_TEAM_AUTOASSIGN );
+
+ UTIL_ClientPrintAll( HUD_PRINTTALK, "#TF_Arena_ClientDisconnect", pPlayer->GetPlayerName(), GetGlobalTeam( pPlayer->GetTeamNumber() )->GetName(), playername );
+
+ if ( pPlayer->DidPlayerJustPlay() == false )
+ {
+ pPlayer->MarkTeamJoinTime();
+ }
+
+ m_hArenaPlayerQueue.FindAndRemove( pPlayer );
+ pPlayer->PlayerJustPlayed( false );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::Arena_ResetLosersScore( bool bResetAll )
+{
+ //Winner gets to keep their score
+ for ( int i = 0; i < GetNumberOfTeams(); i++ )
+ {
+ if ( ( i != GetWinningTeam() && GetWinningTeam() > LAST_SHARED_TEAM ) || bResetAll == true )
+ {
+ GetGlobalTeam( i )->ResetScores();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::Arena_PrepareNewPlayerQueue( bool bResetAll )
+{
+ CUtlVector<CTFPlayer*> pTempPlayerQueue;
+
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( pPlayer && pPlayer->IsHLTV() == false && pPlayer->IsReplay() == false )
+ {
+ if ( ( GetWinningTeam() > LAST_SHARED_TEAM && pPlayer->GetTeamNumber() != GetWinningTeam() && pPlayer->IsReadyToPlay() ) || bResetAll == true )
+ {
+ pTempPlayerQueue.AddToTail( pPlayer );
+ }
+ }
+ }
+
+ //Sort the players going into spectator
+ //Players that have been longer on the server should go in front of newer players
+ //We have to do this since player slots are reused for new clients, so looping through players
+ //using their entindex doesn't work.
+
+ if ( bResetAll == true )
+ {
+ pTempPlayerQueue.Sort( ScramblePlayersSort );
+ }
+ else
+ {
+ pTempPlayerQueue.Sort( SortPlayerSpectatorQueue );
+ }
+
+ for ( int i = 0; i < pTempPlayerQueue.Count(); i++ )
+ {
+ CTFPlayer *pPlayer = pTempPlayerQueue[i];
+
+ if ( pPlayer && pPlayer->IsReadyToPlay() )
+ {
+ //Changing into Spectator Team puts the player at the end of the queue
+ //Use ForceChangeTeam if you want to move them to the FRONT of the queue
+ pPlayer->ChangeTeam( TEAM_SPECTATOR );
+ pPlayer->PlayerJustPlayed( true );
+ }
+ }
+}
+
+#define TF_ARENA_TEAM_COUNT 3
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CTFGameRules::Arena_PlayersNeededForMatch( void )
+{
+ int iMaxPlayers = gpGlobals->maxClients;
+
+ if ( HLTVDirector()->IsActive() )
+ {
+ iMaxPlayers -= 1;
+ }
+
+ int iTeamSize;
+ if ( tf_arena_override_team_size.GetInt() > 0 )
+ iTeamSize = tf_arena_override_team_size.GetInt();
+ else
+ iTeamSize = floor( ( (float)iMaxPlayers / TF_ARENA_TEAM_COUNT ) + 0.5f );
+
+ int iPlayersNeeded = iTeamSize * 2;
+ int iDesiredTeamSize = iTeamSize;
+ int iPlayersInWinningTeam = 0;
+ bool bRebalanceWinners = false;
+
+ int iPlayerNumber = 0;
+
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( pPlayer && pPlayer->IsReadyToPlay() )
+ {
+ iPlayerNumber++;
+ }
+ }
+
+ int iBalancedTeamSize = floor( ((m_hArenaPlayerQueue.Count() + iPlayerNumber ) * 0.5f ) );
+
+ iPlayersNeeded = (iBalancedTeamSize - iPlayerNumber) + iBalancedTeamSize;
+
+ if ( (iPlayersNeeded + iPlayerNumber) > iDesiredTeamSize*2 )
+ {
+ iPlayersNeeded = (iDesiredTeamSize*2) - iPlayerNumber;
+
+ if ( iPlayersNeeded < 0 )
+ {
+ iPlayersNeeded = 0;
+ }
+ }
+
+ // If the last round was won by a team, then let's figure out how many players we need.
+ // Also, if the winning team has more players than are available, then let's have one of them switch teams.
+ if ( GetWinningTeam() > LAST_SHARED_TEAM )
+ {
+ iPlayersInWinningTeam = GetGlobalTFTeam( GetWinningTeam() )->GetNumPlayers();
+
+ if ( iPlayersInWinningTeam > iTeamSize )
+ {
+ bRebalanceWinners = true;
+ iDesiredTeamSize = iTeamSize;
+ }
+ else if ( iPlayersInWinningTeam > iBalancedTeamSize )
+ {
+ bRebalanceWinners = true;
+ iDesiredTeamSize = iBalancedTeamSize;
+ }
+ }
+
+// Msg( "iPlayerNumber: %d - InQueue: %d - iPlayersInWinningTeam: %d - iDesiredTeamSize: %d - iBalancedTeamSize: %d - iPlayersNeeded: %d - bRebalanceWinners %d\n", iPlayerNumber, m_hArenaPlayerQueue.Count(), iPlayersInWinningTeam, iDesiredTeamSize, iBalancedTeamSize, iPlayersNeeded, bRebalanceWinners );
+
+
+ if ( bRebalanceWinners == true )
+ {
+ while ( iPlayersInWinningTeam > iDesiredTeamSize )
+ {
+ CTFPlayer *pBalancedWinner = NULL;
+ float flShortestTeamJoinTime = 9999.9f;
+
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( pPlayer && pPlayer->GetTeamNumber() == GetWinningTeam() && pPlayer->IsHLTV() == false && pPlayer->IsReplay() == false )
+ {
+ if ( bRebalanceWinners == true )
+ {
+ //Find the newest guy that joined this team and flag him.
+ if ( (gpGlobals->curtime - pPlayer->GetTeamJoinTime()) < flShortestTeamJoinTime )
+ {
+ flShortestTeamJoinTime = (gpGlobals->curtime - pPlayer->GetTeamJoinTime());
+ pBalancedWinner = pPlayer;
+ }
+ }
+ }
+ }
+
+ if ( pBalancedWinner )
+ {
+ pBalancedWinner->ForceChangeTeam( TEAM_SPECTATOR );
+ pBalancedWinner->MarkTeamJoinTime();
+
+ if ( iPlayersNeeded < iDesiredTeamSize )
+ {
+ iPlayersNeeded++;
+ }
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_teambalanced_player" );
+ if ( event )
+ {
+ event->SetInt( "player", pBalancedWinner->entindex() );
+ event->SetInt( "team", GetWinningTeam() == TF_TEAM_BLUE ? TF_TEAM_RED : TF_TEAM_BLUE );
+ gameeventmanager->FireEvent( event );
+ }
+
+ // tell people that we've switched this player
+ UTIL_ClientPrintAll( HUD_PRINTTALK, "#game_player_was_team_balanced", pBalancedWinner->GetPlayerName() );
+ }
+
+ iPlayersInWinningTeam = GetGlobalTFTeam( GetWinningTeam() )->GetNumPlayers();
+ }
+ }
+
+ return iPlayersNeeded;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::Arena_CleanupPlayerQueue( void )
+{
+ //One more loop to remove players that are currently playing from the queue
+ //And to mark everyone as not having just played.
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( pPlayer && pPlayer->IsHLTV() == false && pPlayer->IsReplay() == false )
+ {
+ if ( pPlayer->GetTeamNumber() != TEAM_SPECTATOR )
+ {
+ m_hArenaPlayerQueue.FindAndRemove( pPlayer );
+ }
+
+ pPlayer->PlayerJustPlayed( false );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::Arena_RunTeamLogic( void )
+{
+ if ( IsInArenaMode() == false )
+ return;
+
+ if ( IsInWaitingForPlayers() == true )
+ return;
+
+ bool bGameNotReady = !BHavePlayers();
+ bool bStreaksReached = false;
+
+ if ( tf_arena_use_queue.GetBool() == false )
+ {
+ if ( bGameNotReady == true || GetGlobalTFTeam( TF_TEAM_BLUE )->GetNumPlayers() == 0 || GetGlobalTFTeam( TF_TEAM_RED )->GetNumPlayers() == 0 )
+ {
+ State_Transition( GR_STATE_PREGAME );
+ }
+
+ return;
+ }
+
+ if ( tf_arena_max_streak.GetInt() > 0 )
+ {
+ if ( GetWinningTeam() != TEAM_UNASSIGNED )
+ {
+ if ( GetGlobalTFTeam( GetWinningTeam() )->GetScore() >= tf_arena_max_streak.GetInt() )
+ {
+ bStreaksReached = true;
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "arena_match_maxstreak" );
+ if ( event )
+ {
+ event->SetInt( "team", GetWinningTeam() );
+ event->SetInt( "streak", tf_arena_max_streak.GetInt() );
+ gameeventmanager->FireEvent( event );
+ }
+
+ BroadcastSound( 255, "Announcer.AM_TeamScrambleRandom" );
+
+ m_iWinningTeam = TEAM_UNASSIGNED;
+ }
+ }
+ }
+
+ if ( IsInTournamentMode() == false )
+ {
+ Arena_ResetLosersScore( bGameNotReady || bStreaksReached );
+ }
+
+ Arena_PrepareNewPlayerQueue( bGameNotReady || bStreaksReached );
+
+ if ( bGameNotReady == true )
+ {
+ State_Transition( GR_STATE_PREGAME );
+ return;
+ }
+
+ int iPlayersNeeded = Arena_PlayersNeededForMatch();
+
+ //Let's add people to teams
+ //But only do this if there's people in the actual game and teams are unbalanced
+ //(which they should be since winners are in and everyone else is spectating)
+ int iLight, iHeavy;
+
+ if ( AreTeamsUnbalanced( iHeavy, iLight ) == true && iPlayersNeeded > 0 )
+ {
+ if ( iPlayersNeeded > m_hArenaPlayerQueue.Count() )
+ {
+ iPlayersNeeded = m_hArenaPlayerQueue.Count();
+ }
+
+// Msg( "iTeamSize: %d\n", iTeamSize );
+
+ int iTeam = GetWinningTeam();
+
+ int iSwitch = floor( ((GetGlobalTFTeam( GetWinningTeam() )->GetNumPlayers() + iPlayersNeeded) * 0.5f ) - GetGlobalTFTeam( GetWinningTeam() )->GetNumPlayers() );
+
+ if ( GetWinningTeam() == TEAM_UNASSIGNED )
+ {
+ iTeam = TF_TEAM_AUTOASSIGN;
+ }
+
+ //Move people in the queue into a team.
+ for ( int iPlayers = 0; iPlayers < iPlayersNeeded; iPlayers++ )
+ {
+ CTFPlayer *pPlayer = m_hArenaPlayerQueue[iPlayers];
+
+ if ( iPlayers >= iSwitch )
+ {
+ iTeam = TF_TEAM_AUTOASSIGN;
+ }
+
+ if ( pPlayer )
+ {
+ pPlayer->ForceChangeTeam( iTeam );
+
+// Msg( "Moving Player to game: %s - team: %d\n", pPlayer->GetPlayerName(), pPlayer->GetTeamNumber() );
+
+ if ( pPlayer->DidPlayerJustPlay() == false )
+ {
+ pPlayer->MarkTeamJoinTime();
+ }
+ }
+ }
+ }
+
+
+
+ // show the class composition panel
+
+ m_flSendNotificationTime = gpGlobals->curtime + 1.0f;
+
+ Arena_CleanupPlayerQueue();
+ Arena_NotifyTeamSizeChange();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::Arena_NotifyTeamSizeChange( void )
+{
+ int iTeamSize = GetGlobalTFTeam( TF_TEAM_BLUE )->GetNumPlayers();
+
+ if ( iTeamSize == m_iPreviousTeamSize )
+ return;
+
+ if ( m_iPreviousTeamSize == 0 )
+ {
+ m_iPreviousTeamSize = iTeamSize;
+ return;
+ }
+
+ if ( m_iPreviousTeamSize > iTeamSize )
+ {
+ UTIL_ClientPrintAll( HUD_PRINTTALK, "#TF_Arena_TeamSizeDecreased", UTIL_VarArgs( "%d", iTeamSize ) );
+ }
+ else
+ {
+ UTIL_ClientPrintAll( HUD_PRINTTALK, "#TF_Arena_TeamSizeIncreased", UTIL_VarArgs( "%d", iTeamSize ) );
+ }
+
+ m_iPreviousTeamSize = iTeamSize;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::Arena_SendPlayerNotifications( void )
+{
+ int iTeamPlayers = GetGlobalTFTeam( TF_TEAM_RED )->GetNumPlayers() + GetGlobalTFTeam( TF_TEAM_BLUE )->GetNumPlayers();
+ int iNumPlayers = 0;
+
+ m_flSendNotificationTime = 0.0f;
+
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( pPlayer && pPlayer->IsHLTV() == false && pPlayer->IsReplay() == false && pPlayer->GetDesiredPlayerClassIndex() > TF_CLASS_UNDEFINED )
+ {
+ iNumPlayers++;
+
+ if ( pPlayer->GetTeamNumber() == TEAM_SPECTATOR && pPlayer->GetPreviousTeam() != TEAM_UNASSIGNED )
+ {
+ CSingleUserRecipientFilter filter( pPlayer );
+
+ UserMessageBegin( filter, "HudArenaNotify" );
+ WRITE_BYTE( pPlayer->entindex() );
+ WRITE_BYTE( TF_ARENA_NOTIFICATION_SITOUT );
+ MessageEnd();
+ }
+ }
+ }
+
+ if ( iTeamPlayers == iNumPlayers )
+ return;
+
+ int iExtras = iNumPlayers - iTeamPlayers;
+
+ for ( int iTeam = TF_TEAM_RED; iTeam < TF_TEAM_COUNT; iTeam++ )
+ {
+ CUtlVector<CTFPlayer*> pTempPlayerQueue;
+
+ CTFTeam *pTeam = GetGlobalTFTeam( iTeam );
+
+ if ( pTeam )
+ {
+ for ( int iPlayer = 0; iPlayer < pTeam->GetNumPlayers(); iPlayer++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( pTeam->GetPlayer( iPlayer ) );
+
+ if ( pPlayer && pPlayer->IsHLTV() == false && pPlayer->IsReplay() == false )
+ {
+ pTempPlayerQueue.AddToTail( pPlayer );
+ }
+ }
+ }
+
+ pTempPlayerQueue.Sort( SortPlayerSpectatorQueue );
+
+ for ( int i = pTempPlayerQueue.Count(); --i >= 0; )
+ {
+ if ( pTempPlayerQueue.Count() - i > iExtras )
+ continue;
+
+ CTFPlayer *pPlayer = pTempPlayerQueue[i];
+
+ CSingleUserRecipientFilter filter( pPlayer );
+ UserMessageBegin( filter, "HudArenaNotify" );
+ WRITE_BYTE( pPlayer->entindex() );
+ WRITE_BYTE( TF_ARENA_NOTIFICATION_CAREFUL );
+ MessageEnd();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Handle maps that may be in our map cycle/etc changing name or availability
+//-----------------------------------------------------------------------------
+#ifdef GAME_DLL
+void CTFGameRules::OnWorkshopMapUpdated( PublishedFileId_t nUpdatedWorkshopID )
+{
+ // Check if this map is in the mapcycle under a different name, reload mapcycle if so. We want the up-to-date names
+ // in the map cycle as it is used for user-facing things such as votes.
+ CTFMapsWorkshop *pWorkshop = TFMapsWorkshop();
+ if ( pWorkshop )
+ {
+ FOR_EACH_VEC( m_MapList, i )
+ {
+ // Check if this represents a workshop map
+ PublishedFileId_t nWorkshopID = pWorkshop->MapIDFromName( m_MapList[ i ] );
+ if ( nWorkshopID == nUpdatedWorkshopID )
+ {
+ CUtlString newName;
+ if ( pWorkshop->GetMapName( nWorkshopID, newName ) == CTFMapsWorkshop::eName_Canon &&
+ newName != m_MapList[ i ] )
+ {
+ // We can't just fixup the name here, as the primary mapcycle is also mirrored to a string
+ // table. This queues a reload, which will check for workshop names at that point.
+ m_bMapCycleNeedsUpdate = true;
+ break;
+ }
+ }
+ }
+ }
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// Purpose: Hook new map cycle file loads
+//-----------------------------------------------------------------------------
+void CTFGameRules::LoadMapCycleFile( void )
+{
+ BaseClass::LoadMapCycleFile();
+#ifdef GAME_DLL
+ // The underlying LoadMapCycleFileIntoVector fixes up workshop names, but for loading the primary map cycle file, we
+ // also want to tell the workshop to track them. See also: TFGameRules::OnWorkshopMapChanged
+ TrackWorkshopMapsInMapCycle();
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Hook new map cycle file loads
+//-----------------------------------------------------------------------------
+void CTFGameRules::TrackWorkshopMapsInMapCycle( void )
+{
+ CTFMapsWorkshop *pWorkshop = TFMapsWorkshop();
+ if ( pWorkshop )
+ {
+ unsigned int nAddedMaps = 0;
+ unsigned int nWorkshopMaps = 0;
+ FOR_EACH_VEC( m_MapList, i )
+ {
+ // Check if this represents a workshop map
+ PublishedFileId_t nWorkshopID = pWorkshop->MapIDFromName( m_MapList[ i ] );
+ if ( nWorkshopID != k_PublishedFileIdInvalid )
+ {
+ nWorkshopMaps++;
+ // Track it if we're not
+ if ( pWorkshop->AddMap( nWorkshopID ) )
+ {
+ nAddedMaps++;
+ }
+ }
+ }
+
+ if ( nAddedMaps )
+ {
+ Msg( "Tracking %u new workshop maps from map cycle (%u already tracked)\n", nAddedMaps, nWorkshopMaps - nAddedMaps );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Hook new map cycle file loads
+//-----------------------------------------------------------------------------
+void CTFGameRules::LoadMapCycleFileIntoVector( const char *pszMapCycleFile, CUtlVector<char *> &mapList )
+{
+ BaseClass::LoadMapCycleFileIntoVector( pszMapCycleFile, mapList );
+
+#ifdef GAME_DLL
+ // Fixup workshop map names if known. E.g. workshop/12345 -> workshop/cp_foo.ugc12345
+ CTFMapsWorkshop *pWorkshop = TFMapsWorkshop();
+ if ( pWorkshop )
+ {
+ FOR_EACH_VEC( mapList, i )
+ {
+ // Check if this represents a workshop map
+ PublishedFileId_t nWorkshopID = pWorkshop->MapIDFromName( mapList[ i ] );
+ if ( nWorkshopID != k_PublishedFileIdInvalid )
+ {
+ // Workshop map, update name
+ CUtlString newName;
+ pWorkshop->GetMapName( nWorkshopID, newName );
+ if ( newName.Length() )
+ {
+ // Alloc replacement
+ size_t nNewSize = newName.Length() + 1;
+ char *pNew = new char[ nNewSize ];
+ V_strncpy( pNew, newName.Get(), nNewSize );
+
+ // Replace
+ delete [] mapList[ i ];
+ mapList[ i ] = pNew;
+ }
+ }
+ }
+ }
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Server-side vote creation
+//-----------------------------------------------------------------------------
+void CTFGameRules::ManageServerSideVoteCreation( void )
+{
+ if ( gpGlobals->curtime < m_flVoteCheckThrottle )
+ return;
+
+ if ( IsInTournamentMode() )
+ return;
+
+ if ( IsInArenaMode() )
+ return;
+
+ if ( IsInWaitingForPlayers() )
+ return;
+
+ if ( m_bInSetup )
+ return;
+
+ if ( IsInTraining() )
+ return;
+
+ if ( IsInItemTestingMode() )
+ return;
+
+ if ( m_MapList.Count() < 2 )
+ return;
+
+ // Ask players which map they would prefer to play next, based
+ // on "n" lowest playtime from server stats
+
+ ConVarRef sv_vote_issue_nextlevel_allowed( "sv_vote_issue_nextlevel_allowed" );
+ ConVarRef sv_vote_issue_nextlevel_choicesmode( "sv_vote_issue_nextlevel_choicesmode" );
+
+ if ( sv_vote_issue_nextlevel_allowed.GetBool() && sv_vote_issue_nextlevel_choicesmode.GetBool() )
+ {
+ // Don't do this if we already have a nextlevel set
+ if ( nextlevel.GetString() && *nextlevel.GetString() )
+ return;
+
+ if ( !m_bServerVoteOnReset && !m_bVoteCalled )
+ {
+ // If we have any round or win limit, ignore time
+ if ( mp_winlimit.GetInt() || mp_maxrounds.GetInt() )
+ {
+ int nBlueScore = TFTeamMgr()->GetTeam( TF_TEAM_BLUE )->GetScore();
+ int nRedScore = TFTeamMgr()->GetTeam( TF_TEAM_RED)->GetScore();
+ int nWinLimit = mp_winlimit.GetInt();
+ if ( ( nWinLimit - nBlueScore ) == 1 || ( nWinLimit - nRedScore ) == 1 )
+ {
+ m_bServerVoteOnReset = true;
+ }
+
+ int nRoundsPlayed = GetRoundsPlayed();
+ if ( ( mp_maxrounds.GetInt() - nRoundsPlayed ) == 1 )
+ {
+ m_bServerVoteOnReset = true;
+ }
+ }
+ else if ( mp_timelimit.GetInt() > 0 )
+ {
+ int nTimeLeft = GetTimeLeft();
+ if ( nTimeLeft <= 120 && !m_bServerVoteOnReset )
+ {
+ if ( g_voteController )
+ {
+ g_voteController->CreateVote( DEDICATED_SERVER, "nextlevel", "" );
+ }
+ m_bVoteCalled = true;
+ }
+ }
+ }
+ }
+
+ m_flVoteCheckThrottle = gpGlobals->curtime + 0.5f;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Figures out how much money to put in a custom currency pack drop
+//-----------------------------------------------------------------------------
+int CTFGameRules::CalculateCurrencyAmount_CustomPack( int nAmount )
+{
+ // Entities and events that specify a custom currency value should pass in the amount
+ // they're worth, and we figure out if there's enough currency to generate a pack.
+ // If the amount passed in isn't enough to generate a pack, we store it in an accumulator.
+
+ int nMinDrop = kMVM_CurrencyPackMinSize;
+ if ( nMinDrop > 1 )
+ {
+ // If we're on the last spawn, drop everything
+ if ( TFObjectiveResource()->GetMannVsMachineWaveEnemyCount() == 1 )
+ {
+ nMinDrop = m_nCurrencyAccumulator + nAmount;
+ m_nCurrencyAccumulator = 0;
+ return nMinDrop;
+ }
+
+ // If we're passing in a value above mindrop, just drop it
+ if ( nAmount >= nMinDrop )
+ return nAmount;
+
+ // Accumulate currency if we're getting values below nMinDrop
+ m_nCurrencyAccumulator += nAmount;
+ if ( m_nCurrencyAccumulator >= nMinDrop )
+ {
+ m_nCurrencyAccumulator -= nMinDrop;
+ //DevMsg( "*MIN REACHED* -- %d left\n", m_nCurrencyAccumulator );
+ return nMinDrop;
+ }
+ else
+ {
+ // We don't have enough yet, drop nothing
+ //DevMsg( "*STORE* -- %d stored\n", m_nCurrencyAccumulator );
+ return 0;
+ }
+ }
+ else
+ {
+ // We don't have a PopManager - return the amount passed in
+ return nAmount;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Figures out how much to give for pre-definied events or types
+//-----------------------------------------------------------------------------
+int CTFGameRules::CalculateCurrencyAmount_ByType( CurrencyRewards_t nType )
+{
+ // CUSTOM values are usually determined by CalculateCurrencyAmount_CustomPack() and set via CCurrencyPack::SetValue()
+ Assert ( nType != TF_CURRENCY_PACK_CUSTOM );
+
+ int nAmount = 0;
+
+ switch ( nType )
+ {
+ case TF_CURRENCY_KILLED_PLAYER:
+ nAmount = 40;
+ break;
+
+ case TF_CURRENCY_KILLED_OBJECT:
+ nAmount = 40;
+ break;
+
+ case TF_CURRENCY_ASSISTED_PLAYER:
+ nAmount = 20;
+ break;
+
+ case TF_CURRENCY_BONUS_POINTS:
+ nAmount = 1;
+ break;
+
+ case TF_CURRENCY_CAPTURED_OBJECTIVE:
+ nAmount = 100;
+ break;
+
+ case TF_CURRENCY_ESCORT_REWARD:
+ nAmount = 10;
+ break;
+
+ case TF_CURRENCY_PACK_SMALL:
+ nAmount = 5;
+ break;
+
+ case TF_CURRENCY_PACK_MEDIUM:
+ nAmount = 10;
+ break;
+
+ case TF_CURRENCY_PACK_LARGE:
+ nAmount = 25;
+ break;
+
+ case TF_CURRENCY_TIME_REWARD:
+ nAmount = 5;
+ break;
+
+ case TF_CURRENCY_WAVE_COLLECTION_BONUS:
+ nAmount = 100;
+ break;
+
+ default:
+ Assert( 0 ); // Unknown type
+ };
+
+ return nAmount;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Gives money directly to a team or specific player
+//-----------------------------------------------------------------------------
+int CTFGameRules::DistributeCurrencyAmount( int nAmount, CTFPlayer *pTFPlayer /* = NULL */, bool bShared /* = true */, bool bCountAsDropped /*= false */, bool bIsBonus /*= false */ )
+{
+ // Group distribution (default)
+ if ( bShared )
+ {
+ CUtlVector<CTFPlayer *> playerVector;
+
+ if ( IsMannVsMachineMode() )
+ {
+ CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS );
+ }
+#ifdef STAGING_ONLY
+ else if ( IsBountyMode() )
+ {
+ // We require a player in order to award the proper team
+ if ( pTFPlayer )
+ {
+ CollectPlayers( &playerVector, pTFPlayer->GetTeamNumber() );
+ }
+ }
+#endif // STAGING_ONLY
+
+ // Money
+ FOR_EACH_VEC( playerVector, i )
+ {
+ if ( playerVector[i] )
+ {
+#ifdef STAGING_ONLY
+ if ( IsBountyMode() )
+ {
+ // Check for a cap
+ int nLimit = tf_bountymode_currency_limit.GetInt();
+ if ( nLimit > 0 )
+ {
+ int nNewCurrency = nAmount + pTFPlayer->GetCurrency();
+ if ( nNewCurrency > nLimit )
+ {
+ int nDelta = nNewCurrency - nLimit;
+ if ( nDelta )
+ {
+ nAmount -= nDelta;
+ }
+ }
+ }
+ }
+#endif // STAGING_ONLY
+
+ playerVector[i]->AddCurrency( nAmount );
+ }
+ }
+ }
+ // Individual distribution
+ else if ( pTFPlayer )
+ {
+#ifdef STAGING_ONLY
+ if ( IsBountyMode() )
+ {
+ // Check for a cap
+ int nLimit = tf_bountymode_currency_limit.GetInt();
+ if ( nLimit > 0 )
+ {
+ int nNewCurrency = nAmount + pTFPlayer->GetCurrency();
+ if ( nNewCurrency > nLimit )
+ {
+ int nDelta = nNewCurrency - nLimit;
+ if ( nDelta )
+ {
+ nAmount -= nDelta;
+ }
+ }
+ }
+ }
+#endif // STAGING_ONLY
+
+ pTFPlayer->AddCurrency( nAmount );
+ }
+
+ // Accounting
+ if ( IsMannVsMachineMode() && g_pPopulationManager )
+ {
+ g_pPopulationManager->OnCurrencyCollected( nAmount, bCountAsDropped, bIsBonus );
+ }
+
+ return nAmount;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::RoundRespawn( void )
+{
+#ifdef GAME_DLL
+ m_hasSpawnedToy = false;
+ for ( int i = 0; i < TF_TEAM_COUNT; i++ )
+ {
+ m_bHasSpawnedSoccerBall[i] = false;
+ }
+#endif // GAME_DLL
+
+ // remove any buildings, grenades, rockets, etc. the player put into the world
+ RemoveAllProjectilesAndBuildings();
+
+ // re-enable any sentry guns the losing team has built (and not hidden!)
+ for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i )
+ {
+ CBaseObject *pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] );
+ if ( pObj->ObjectType() == OBJ_SENTRYGUN && pObj->IsEffectActive( EF_NODRAW ) == false && pObj->GetTeamNumber() != m_iWinningTeam )
+ {
+ pObj->SetDisabled( false );
+ }
+ }
+
+#ifdef TF_RAID_MODE
+ // Raid mode: clean up any Red buildings that might be left behind from the previous round
+ if ( IsRaidMode() )
+ {
+ CTFTeam *pTeam = TFTeamMgr()->GetTeam( TF_TEAM_RED );
+ if ( pTeam )
+ {
+ int nTeamObjectCount = pTeam->GetNumObjects();
+
+ for ( int iObject = 0; iObject < nTeamObjectCount; ++iObject )
+ {
+ CBaseObject *pObj = pTeam->GetObject( iObject );
+
+ if ( pObj )
+ {
+ pObj->SetThink( &CBaseEntity::SUB_Remove );
+ pObj->SetNextThink( gpGlobals->curtime );
+ pObj->SetTouch( NULL );
+ pObj->AddEffects( EF_NODRAW );
+ }
+ }
+ }
+ }
+ else if ( IsBossBattleMode() )
+ {
+ // unspawn entire red team
+ CTeam *defendingTeam = GetGlobalTeam( TF_TEAM_RED );
+ int i;
+ for( i=0; i<defendingTeam->GetNumPlayers(); ++i )
+ {
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", defendingTeam->GetPlayer(i)->GetUserID() ) );
+ }
+ }
+#endif // TF_RAID_MODE
+
+ if ( IsInTournamentMode() == false )
+ {
+ Arena_RunTeamLogic();
+ }
+
+ m_bArenaFirstBlood = false;
+
+ // reset the flag captures
+ int nTeamCount = TFTeamMgr()->GetTeamCount();
+ for ( int iTeam = FIRST_GAME_TEAM; iTeam < nTeamCount; ++iTeam )
+ {
+ CTFTeam *pTeam = GetGlobalTFTeam( iTeam );
+ if ( !pTeam )
+ continue;
+
+ pTeam->SetFlagCaptures( 0 );
+ }
+
+ if ( !IsMannVsMachineMode() )
+ {
+ IGameEvent *event = gameeventmanager->CreateEvent( "scorestats_accumulated_update" );
+ if ( event )
+ {
+ gameeventmanager->FireEvent( event );
+ }
+ }
+
+ // reset player per-round stats
+ CTF_GameStats.ResetRoundStats();
+
+ BaseClass::RoundRespawn();
+
+ // ** AFTER WE'VE BEEN THROUGH THE ROUND RESPAWN, SHOW THE ROUNDINFO PANEL
+ if ( !IsInWaitingForPlayers() )
+ {
+ ShowRoundInfoPanel();
+ }
+
+ // We've hit some condition where a server-side vote should be called on respawn
+ if ( m_bServerVoteOnReset )
+ {
+ if ( g_voteController )
+ {
+ g_voteController->CreateVote( DEDICATED_SERVER, "nextlevel", "" );
+ }
+ m_bVoteCalled = true;
+ m_bServerVoteOnReset = false;
+ }
+}
+
+#ifdef GAME_DLL
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldSwitchTeams( void )
+{
+ if ( IsPVEModeActive() )
+ return false;
+
+ return BaseClass::ShouldSwitchTeams();
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldScrambleTeams( void )
+{
+ if ( IsPVEModeActive() )
+ return false;
+
+ if ( IsCompetitiveMode() )
+ return false;
+
+ return BaseClass::ShouldScrambleTeams();
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::ClientCommandKeyValues( edict_t *pEntity, KeyValues *pKeyValues )
+{
+ CTFPlayer *pTFPlayer = ToTFPlayer( CBaseEntity::Instance( pEntity ) );
+
+ if ( !pTFPlayer )
+ return;
+
+ char const *pszCommand = pKeyValues->GetName();
+ if ( pszCommand && pszCommand[0] )
+ {
+ if ( FStrEq( pszCommand, "FreezeCamTaunt" ) )
+ {
+ CTFPlayer *pAchiever = ToTFPlayer( UTIL_PlayerByUserId( pKeyValues->GetInt( "achiever" ) ) );
+ if ( pAchiever )
+ {
+ const char *pszCommand = pKeyValues->GetString( "command" );
+ if ( pszCommand && pszCommand[0] )
+ {
+ int nGibs = pKeyValues->GetInt( "gibs" );
+
+ if ( FStrEq( pszCommand, "freezecam_taunt" ) )
+ {
+ CheckTauntAchievement( pAchiever, nGibs, g_TauntCamAchievements );
+ CheckTauntAchievement( pAchiever, nGibs, g_TauntCamAchievements2 );
+ HatAndMiscEconEntities_OnOwnerKillEaterEventNoParter( pAchiever, kKillEaterEvent_KillcamTaunts );
+ }
+ else if ( FStrEq( pszCommand, "freezecam_tauntrag" ) )
+ {
+ CheckTauntAchievement( pAchiever, nGibs, g_TauntCamRagdollAchievements );
+ }
+ else if ( FStrEq( pszCommand, "freezecam_tauntgibs" ) )
+ {
+ CheckTauntAchievement( pAchiever, nGibs, g_TauntCamAchievements );
+ }
+ else if ( FStrEq( pszCommand, "freezecam_tauntsentry" ) )
+ {
+ // Maybe should also require a taunt? Currently too easy to get?
+ pAchiever->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_FREEZECAM_SENTRY );
+ }
+ }
+ }
+ }
+ else if ( FStrEq( pszCommand, "UsingVRHeadset" ) )
+ {
+ pTFPlayer->SetUsingVRHeadset( true );
+ }
+ else if ( FStrEq( pszCommand, "TestItems" ) )
+ {
+ pTFPlayer->ItemTesting_Start( pKeyValues );
+ }
+ else if ( FStrEq( pszCommand, "TestItemsBotUpdate" ) )
+ {
+ ItemTesting_SetupFromKV( pKeyValues );
+ }
+ else if ( FStrEq( pszCommand, "MVM_Upgrade" ) )
+ {
+ if ( GameModeUsesUpgrades() )
+ {
+#ifndef STAGING_ONLY
+ if ( IsMannVsMachineMode() )
+ {
+ if ( sv_cheats && !sv_cheats->GetBool() && !pTFPlayer->m_Shared.IsInUpgradeZone() )
+ return;
+ }
+#endif //!STAGING_ONLY
+
+ if ( g_hUpgradeEntity )
+ {
+ // First sell everything we want to sell
+ KeyValues *pSubKey = pKeyValues->GetFirstTrueSubKey();
+ while ( pSubKey )
+ {
+ int iCount = pSubKey->GetInt("count");
+ if ( iCount < 0 )
+ {
+ int iItemSlot = pSubKey->GetInt("itemslot");
+ int iUpgrade = pSubKey->GetInt("upgrade");
+ bool bFree = pSubKey->GetBool( "free", false );
+
+ while ( iCount < 0 )
+ {
+ g_hUpgradeEntity->PlayerPurchasingUpgrade( pTFPlayer, iItemSlot, iUpgrade, true, bFree );
+ ++iCount;
+ }
+ }
+
+ pSubKey = pSubKey->GetNextTrueSubKey();
+ }
+
+ // Now buy everything we want to buy
+ pSubKey = pKeyValues->GetFirstTrueSubKey();
+ while ( pSubKey )
+ {
+ int iCount = pSubKey->GetInt("count");
+ if ( iCount > 0 )
+ {
+ int iItemSlot = pSubKey->GetInt("itemslot");
+ int iUpgrade = pSubKey->GetInt("upgrade");
+ bool bFree = ( sv_cheats && sv_cheats->GetBool() ) ? pSubKey->GetBool( "free", false ) : false; // Never let a client set "free" without sv_cheats 1
+
+ while ( iCount > 0 )
+ {
+ g_hUpgradeEntity->PlayerPurchasingUpgrade( pTFPlayer, iItemSlot, iUpgrade, false, bFree );
+ --iCount;
+ }
+ }
+
+ pSubKey = pSubKey->GetNextTrueSubKey();
+ }
+ }
+ }
+ }
+ else if ( FStrEq( pszCommand, "MvM_UpgradesBegin" ) )
+ {
+ pTFPlayer->BeginPurchasableUpgrades();
+ }
+ else if ( FStrEq( pszCommand, "MvM_UpgradesDone" ) )
+ {
+ pTFPlayer->EndPurchasableUpgrades();
+
+ if ( IsMannVsMachineMode() && pKeyValues->GetInt( "num_upgrades", 0 ) > 0 )
+ {
+ pTFPlayer->SpeakConceptIfAllowed( MP_CONCEPT_MVM_UPGRADE_COMPLETE );
+ }
+ }
+ else if ( FStrEq( pszCommand, "MVM_Revive_Response" ) )
+ {
+ CTFReviveMarker *pReviveMarker = pTFPlayer->GetReviveMarker();
+ if ( pReviveMarker )
+ {
+ if ( pKeyValues->GetBool( "accepted", 0 ) )
+ {
+ pReviveMarker->ReviveOwner();
+ }
+ else
+ {
+ // They hit cancel after their spawn timer was up
+ if ( HasPassedMinRespawnTime( pTFPlayer ) )
+ {
+ pTFPlayer->ForceRespawn();
+ }
+
+ UTIL_Remove( pReviveMarker );
+ }
+ }
+ }
+ else if ( FStrEq( pszCommand, "MVM_Respec" ) )
+ {
+ if ( GameModeUsesUpgrades() && IsMannVsMachineRespecEnabled() && CanPlayerUseRespec( pTFPlayer ) )
+ {
+#ifndef STAGING_ONLY
+ if ( IsMannVsMachineMode() )
+ {
+ if ( sv_cheats && !sv_cheats->GetBool() && !pTFPlayer->m_Shared.IsInUpgradeZone() )
+ return;
+ }
+#endif //!STAGING_ONLY
+
+ if ( g_hUpgradeEntity && g_pPopulationManager )
+ {
+ // Consume a respec credit
+ g_pPopulationManager->RemoveRespecFromPlayer( pTFPlayer );
+
+ // Remove the appropriate upgrade info from upgrade histories
+ g_pPopulationManager->RemovePlayerAndItemUpgradesFromHistory( pTFPlayer );
+
+ // Remove upgrade attributes from the player and their items
+ g_hUpgradeEntity->GrantOrRemoveAllUpgrades( pTFPlayer, true );
+
+ pTFPlayer->ForceRespawn();
+ }
+ }
+ }
+ else if ( FStrEq( pszCommand, "use_action_slot_item_server" ) )
+ {
+ if ( pTFPlayer->ShouldRunRateLimitedCommand( "use_action_slot_item_server" ) )
+ {
+ pTFPlayer->UseActionSlotItemPressed();
+ pTFPlayer->UseActionSlotItemReleased();
+ }
+ }
+ else if ( FStrEq( pszCommand, "+use_action_slot_item_server" ) )
+ {
+ if ( !pTFPlayer->IsUsingActionSlot() )
+ {
+ pTFPlayer->UseActionSlotItemPressed();
+ }
+ }
+ else if ( FStrEq( pszCommand, "-use_action_slot_item_server" ) )
+ {
+ if ( pTFPlayer->IsUsingActionSlot() )
+ {
+ pTFPlayer->UseActionSlotItemReleased();
+ }
+ }
+ else if ( FStrEq( pszCommand, "+inspect_server" ) )
+ {
+ pTFPlayer->InspectButtonPressed();
+ }
+ else if ( FStrEq( pszCommand, "-inspect_server" ) )
+ {
+ pTFPlayer->InspectButtonReleased();
+ }
+ else if ( FStrEq( pszCommand, "cl_drawline" ) )
+ {
+ BroadcastDrawLine( pTFPlayer, pKeyValues );
+ }
+ else if ( FStrEq( pszCommand, "AutoBalanceVolunteerReply" ) )
+ {
+ if ( TFAutoBalance() )
+ {
+ TFAutoBalance()->ReplyReceived( pTFPlayer, pKeyValues->GetBool( "response", false ) );
+ }
+ }
+ else
+ {
+ BaseClass::ClientCommandKeyValues( pEntity, pKeyValues );
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::BroadcastDrawLine( CTFPlayer *pTFPlayer, KeyValues *pKeyValues )
+{
+ int paneltype = clamp( pKeyValues->GetInt( "panel", DRAWING_PANEL_TYPE_NONE ), DRAWING_PANEL_TYPE_NONE, DRAWING_PANEL_TYPE_MAX - 1 );
+
+ if ( paneltype >= DRAWING_PANEL_TYPE_MATCH_SUMMARY )
+ {
+ int linetype = clamp( pKeyValues->GetInt( "line", 0 ), 0, 1 );
+ float x = pKeyValues->GetFloat( "x", 0.f );
+ float y = pKeyValues->GetFloat( "y", 0.f );
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "cl_drawline" );
+ if ( event )
+ {
+ event->SetInt( "player", pTFPlayer->entindex() );
+ event->SetInt( "panel",paneltype );
+ event->SetInt( "line", linetype );
+ event->SetFloat( "x", x );
+ event->SetFloat( "y", y );
+
+ gameeventmanager->FireEvent( event );
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::RespawnTeam( int iTeam )
+{
+ BaseClass::RespawnTeam( iTeam );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SpawnPlayerInHell( CTFPlayer *pPlayer, const char *pszSpawnPointName )
+{
+ Vector vTeleportPosition;
+ QAngle qTeleportAngles;
+
+ int iCachedLocations = m_mapTeleportLocations.Find( MAKE_STRING( pszSpawnPointName ) );
+ if ( m_mapTeleportLocations.IsValidIndex( iCachedLocations ) )
+ {
+ CUtlVector< TeleportLocation_t > *pLocations = m_mapTeleportLocations[iCachedLocations];
+ Assert( pLocations );
+ if ( !pLocations )
+ return;
+
+ const TeleportLocation_t& location = pLocations->Element( RandomInt( 0, pLocations->Count() - 1 ) );
+ vTeleportPosition = location.m_vecPosition;
+ qTeleportAngles = location.m_qAngles;
+ }
+ else
+ {
+ CUtlVector< CBaseEntity* > m_vecPossibleSpawns;
+ CBaseEntity* pSpawn = NULL;
+ while( (pSpawn = gEntList.FindEntityByName( pSpawn, pszSpawnPointName ) ) != NULL )
+ {
+ m_vecPossibleSpawns.AddToTail( pSpawn );
+ }
+
+ // There had better be a spawnpoint in this map!
+ Assert( m_vecPossibleSpawns.Count() );
+ if ( m_vecPossibleSpawns.Count() == 0 )
+ return;
+
+ // Randomly choose one
+ pSpawn = m_vecPossibleSpawns[ RandomInt( 0, m_vecPossibleSpawns.Count() - 1 ) ];
+ vTeleportPosition = pSpawn->GetAbsOrigin();
+ qTeleportAngles = pSpawn->GetAbsAngles();
+ }
+
+ // Teleport them to hell
+ pPlayer->Teleport( &vTeleportPosition, &qTeleportAngles, &vec3_origin );
+ pPlayer->pl.v_angle = qTeleportAngles;
+
+ // Send us to hell as a ghost!
+ pPlayer->m_Shared.AddCond( TF_COND_HALLOWEEN_GHOST_MODE );
+ pPlayer->m_Shared.AddCond( TF_COND_HALLOWEEN_IN_HELL );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+extern ISoundEmitterSystemBase *soundemitterbase;
+void CTFGameRules::PlayHelltowerAnnouncerVO( int iRedLine, int iBlueLine )
+{
+ static float flRedAnnouncerTalkingUntil = 0.00f, flBlueAnnouncerTalkingUntil = 0.00f;
+
+ // 01 is the first line for the VO
+ int iRandomVORed = RandomInt( 1, g_pszHelltowerAnnouncerLines[iRedLine].m_nCount );
+ int iRandomVOBlue = RandomInt( 1, g_pszHelltowerAnnouncerLines[iBlueLine].m_nCount );
+
+ // the misc lines should match up so we'll play the same lines to both teams
+ if ( ( iRedLine <= HELLTOWER_VO_BLUE_MISC_RARE ) && ( iBlueLine <= HELLTOWER_VO_BLUE_MISC_RARE ) )
+ {
+ // can we safely set blue to the same value?
+ if ( iRandomVORed <= g_pszHelltowerAnnouncerLines[iBlueLine].m_nCount )
+ {
+ iRandomVOBlue = iRandomVORed;
+ }
+ }
+
+ char szRedAudio[128];
+ char szBlueAudio[128];
+ V_sprintf_safe( szRedAudio, g_pszHelltowerAnnouncerLines[iRedLine].m_pszFormatString, iRandomVORed );
+ V_sprintf_safe( szBlueAudio, g_pszHelltowerAnnouncerLines[iBlueLine].m_pszFormatString, iRandomVOBlue );
+
+ bool bForceVO = false;
+ switch (iRedLine)
+ {
+ case HELLTOWER_VO_RED_WIN:
+ case HELLTOWER_VO_RED_WIN_RARE:
+ case HELLTOWER_VO_RED_LOSE:
+ case HELLTOWER_VO_RED_LOSE_RARE:
+ bForceVO = true;
+ }
+ switch (iBlueLine)
+ {
+ case HELLTOWER_VO_BLUE_WIN:
+ case HELLTOWER_VO_BLUE_WIN_RARE:
+ case HELLTOWER_VO_BLUE_LOSE:
+ case HELLTOWER_VO_BLUE_LOSE_RARE:
+ bForceVO = true;
+ }
+
+ CSoundParameters params;
+ float flSoundDuration = 0;
+
+ if ( gpGlobals->curtime > flRedAnnouncerTalkingUntil || bForceVO )
+ {
+ BroadcastSound( TF_TEAM_RED, szRedAudio );
+ if ( soundemitterbase->GetParametersForSound( szRedAudio, params, GENDER_NONE ) )
+ {
+ //flSoundDuration = enginesound->GetSoundDuration( params.soundname );
+ flRedAnnouncerTalkingUntil = gpGlobals->curtime + flSoundDuration;
+ }
+ else
+ {
+ flRedAnnouncerTalkingUntil = 0.00;
+ }
+ }
+ if ( gpGlobals->curtime > flBlueAnnouncerTalkingUntil || bForceVO )
+ {
+ BroadcastSound( TF_TEAM_BLUE, szBlueAudio );
+ if ( soundemitterbase->GetParametersForSound( szBlueAudio, params, GENDER_NONE ) )
+ {
+ //flSoundDuration = enginesound->GetSoundDuration( params.soundname );
+ flBlueAnnouncerTalkingUntil = gpGlobals->curtime + flSoundDuration;
+ }
+ else
+ {
+ flBlueAnnouncerTalkingUntil = 0.00;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Based on connected match players, chooses the 3 maps players can
+// vote on as their next map
+//-----------------------------------------------------------------------------
+void CTFGameRules::ChooseNextMapVoteOptions()
+{
+ // Copy chosen maps into the actual fields we're networking to clients
+ for( int i=0; i < NEXT_MAP_VOTE_OPTIONS; ++i )
+ {
+ m_nNextMapVoteOptions.Set( i, GTFGCClientSystem()->GetNextMapVoteByIndex( i )->m_nDefIndex );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::CheckHelltowerCartAchievement( int iTeam )
+{
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, iTeam );
+
+ FOR_EACH_VEC( playerVector, i )
+ {
+ CTFPlayer *pPlayer = playerVector[i];
+ if ( pPlayer && ( pPlayer->GetObserverMode() <= OBS_MODE_DEATHCAM ) ) // they might be killed by the explosion, so check if they are OBS_MODE_NONE OR OBS_MODE_DEATHCAM
+ {
+ CTriggerAreaCapture *pAreaTrigger = pPlayer->GetControlPointStandingOn();
+ if ( pAreaTrigger && pAreaTrigger->TeamCanCap( iTeam ) )
+ {
+ pPlayer->AwardAchievement( ACHIEVEMENT_TF_HALLOWEEN_HELLTOWER_KILL_BROTHERS );
+ }
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::HandleMapEvent( inputdata_t &inputdata )
+{
+ if ( FStrEq( "sd_doomsday", STRING( gpGlobals->mapname ) ) )
+ {
+ // 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;
+ }
+ }
+
+ // make sure it's being carried by one of the teams
+ if ( pFlag && pFlag->IsStolen() )
+ {
+ CTFPlayer *pFlagCarrier = ToTFPlayer( pFlag->GetOwnerEntity() );
+ if ( pFlagCarrier )
+ {
+ // let everyone know which team has opened the rocket
+ IGameEvent *event = gameeventmanager->CreateEvent( "doomsday_rocket_open" );
+ if ( event )
+ {
+ event->SetInt( "team", pFlagCarrier->GetTeamNumber() );
+ gameeventmanager->FireEvent( event );
+ }
+ }
+ }
+ }
+ else if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ const char *pszEvent = inputdata.value.String();
+ if ( FStrEq( pszEvent, "midnight" ) )
+ {
+ HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_PLAYER_HELLTOWER_MIDNIGHT );
+ }
+ else if ( FStrEq( pszEvent, "horde" ) )
+ {
+ HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_PLAYER_SKELETON_KING_APPEAR );
+ PlayHelltowerAnnouncerVO( HELLTOWER_VO_RED_SKELETON_KING, HELLTOWER_VO_BLUE_SKELETON_KING );
+ }
+ else if ( FStrEq( pszEvent, "red_capture" ) )
+ {
+ CheckHelltowerCartAchievement( TF_TEAM_RED );
+ if ( RandomFloat( 0, 1 ) < HELLTOWER_RARE_LINE_CHANCE )
+ {
+ PlayHelltowerAnnouncerVO( HELLTOWER_VO_RED_WIN_RARE, HELLTOWER_VO_BLUE_LOSE_RARE );
+ }
+ else
+ {
+ PlayHelltowerAnnouncerVO( HELLTOWER_VO_RED_WIN, HELLTOWER_VO_BLUE_LOSE );
+ }
+ }
+ else if ( FStrEq( pszEvent, "blue_capture" ) )
+ {
+ CheckHelltowerCartAchievement( TF_TEAM_BLUE );
+ if ( RandomFloat( 0, 1 ) < HELLTOWER_RARE_LINE_CHANCE )
+ {
+ PlayHelltowerAnnouncerVO( HELLTOWER_VO_RED_LOSE_RARE, HELLTOWER_VO_BLUE_WIN_RARE );
+ }
+ else
+ {
+ PlayHelltowerAnnouncerVO( HELLTOWER_VO_RED_LOSE, HELLTOWER_VO_BLUE_WIN );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetCustomUpgradesFile( inputdata_t &inputdata )
+{
+ const char *pszPath = inputdata.value.String();
+
+ // Reload
+ g_MannVsMachineUpgrades.LoadUpgradesFileFromPath( pszPath );
+
+ // Tell future clients to load from this path
+ V_strncpy( m_pszCustomUpgradesFile.GetForModify(), pszPath, MAX_PATH );
+
+ // Tell connected clients to reload
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "upgrades_file_changed" );
+ if ( pEvent )
+ {
+ pEvent->SetString( "path", pszPath );
+ gameeventmanager->FireEvent( pEvent );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldWaitToStartRecording( void )
+{
+ if ( IsMannVsMachineMode() )
+ {
+ // Don't wait for the WaitingForPlayers period to end if we're MvM
+ return false;
+ }
+
+ return BaseClass::ShouldWaitToStartRecording();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: return true if this flag is currently allowed to be captured
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CanFlagBeCaptured( CBaseEntity *pOther )
+{
+ if ( pOther && ( tf_flag_return_on_touch.GetBool() || IsPowerupMode() ) )
+ {
+ for ( int i = 0; i < ICaptureFlagAutoList::AutoList().Count(); ++i )
+ {
+ CCaptureFlag *pListFlag = static_cast<CCaptureFlag*>( ICaptureFlagAutoList::AutoList()[i] );
+ if ( ( pListFlag->GetType() == TF_FLAGTYPE_CTF ) && !pListFlag->IsDisabled() && ( pOther->GetTeamNumber() == pListFlag->GetTeamNumber() ) && !pListFlag->IsHome() )
+ return false;
+ }
+ }
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: return true if the game is in a state where both flags are stolen and poisonous
+//-----------------------------------------------------------------------------
+bool CTFGameRules::PowerupModeFlagStandoffActive( void )
+{
+ if ( IsPowerupMode() )
+ {
+ int nQualifyingFlags = 0; // All flags need to be stolen and poisonous (poisonous time delay gives the flag carriers a chance to get out of the enemy base)
+ int nEnabledFlags = 0; // Some flags might be in the autolist but be out of play. We don't want them included in the count
+ for ( int i = 0; i < ICaptureFlagAutoList::AutoList().Count(); ++i )
+ {
+ CCaptureFlag *pFlag = static_cast<CCaptureFlag*>( ICaptureFlagAutoList::AutoList()[i] );
+ if ( !pFlag->IsDisabled() )
+ nEnabledFlags++;
+ if ( pFlag->IsPoisonous() && pFlag->IsStolen() )
+ nQualifyingFlags++;
+ }
+ if ( nQualifyingFlags == nEnabledFlags )
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+void CTFGameRules::TeleportPlayersToTargetEntities( int iTeam, const char *pszEntTargetName, CUtlVector< CTFPlayer * > *pTeleportedPlayers )
+{
+ CUtlVector< CTFPlayer * > vecPlayers;
+ CUtlVector< CTFPlayer * > vecRandomOrderPlayers;
+
+ CollectPlayers( &vecPlayers, iTeam );
+
+ FOR_EACH_VEC( vecPlayers, i )
+ {
+ CTFPlayer *pPlayer = vecPlayers[i];
+
+ // Skip players not on a team or who have not chosen a class
+ if ( ( pPlayer->GetTeamNumber() != TF_TEAM_RED && pPlayer->GetTeamNumber() != TF_TEAM_BLUE )
+ || pPlayer->IsPlayerClass( TF_CLASS_UNDEFINED ) )
+ continue;
+
+ vecRandomOrderPlayers.AddToTail( pPlayer );
+ }
+
+ // Randomize the order so players dont go to the same spot every time
+ vecRandomOrderPlayers.Shuffle();
+
+ string_t sName = MAKE_STRING( pszEntTargetName );
+ int iCachedLocationIndex = m_mapTeleportLocations.Find( sName );
+ CUtlVector< TeleportLocation_t > *pCachedLocations = NULL;
+ // is there any cached entities to teleport to
+ if ( m_mapTeleportLocations.IsValidIndex( iCachedLocationIndex ) )
+ {
+ pCachedLocations = m_mapTeleportLocations[iCachedLocationIndex];
+ }
+
+
+ int iCurrentTeleportLocation = 0;
+ CBaseEntity *pSpawnPoint = NULL;
+ FOR_EACH_VEC_BACK( vecRandomOrderPlayers, i )
+ {
+ // don't do anything if we run out of spawn point
+ Vector vTeleportPosition;
+ QAngle qTeleportAngles;
+
+ // if we have cached locations, use them
+ if ( pCachedLocations )
+ {
+ Assert( iCurrentTeleportLocation < pCachedLocations->Count() );
+ if ( iCurrentTeleportLocation < pCachedLocations->Count() )
+ {
+ const TeleportLocation_t& location = pCachedLocations->Element( iCurrentTeleportLocation );
+ vTeleportPosition = location.m_vecPosition;
+ qTeleportAngles = location.m_qAngles;
+ iCurrentTeleportLocation++;
+ }
+ else
+ {
+ // we need to add more teleport location in the map for players to teleport to
+ continue;
+ }
+ }
+ else // use old search for entities by name
+ {
+ pSpawnPoint = gEntList.FindEntityByName( pSpawnPoint, pszEntTargetName );
+
+ Assert( pSpawnPoint );
+ if ( pSpawnPoint )
+ {
+ vTeleportPosition = pSpawnPoint->GetAbsOrigin();
+ qTeleportAngles = pSpawnPoint->GetAbsAngles();
+ }
+ else
+ {
+ // we need to add more teleport location in the map for players to teleport to
+ continue;
+ }
+ }
+
+ CTFPlayer *pPlayer = vecRandomOrderPlayers[ i ];
+ pPlayer->m_Shared.RemoveAllCond();
+
+ // Respawn dead players
+ if ( !pPlayer->IsAlive() )
+ {
+ pPlayer->ForceRespawn();
+ }
+
+ // Unzoom if we are a sniper zoomed!
+ pPlayer->m_Shared.InstantlySniperUnzoom();
+
+ // Teleport
+ pPlayer->Teleport( &vTeleportPosition, &qTeleportAngles, &vec3_origin );
+ pPlayer->SnapEyeAngles( qTeleportAngles );
+
+ // Force client to update all view angles (including kart and taunt yaw)
+ pPlayer->ForcePlayerViewAngles( qTeleportAngles );
+
+ // fill in the teleported player vector
+ if ( pTeleportedPlayers )
+ {
+ pTeleportedPlayers->AddToTail( pPlayer );
+ }
+ }
+}
+
+#endif // GAME_DLL
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::InternalHandleTeamWin( int iWinningTeam )
+{
+ // remove any spies' disguises and make them visible (for the losing team only)
+ // and set the speed for both teams (winners get a boost and losers have reduced speed)
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( pPlayer )
+ {
+ if ( pPlayer->GetTeamNumber() > LAST_SHARED_TEAM )
+ {
+ if ( pPlayer->GetTeamNumber() != iWinningTeam )
+ {
+ pPlayer->RemoveInvisibility();
+// pPlayer->RemoveDisguise();
+
+ if ( pPlayer->HasTheFlag() )
+ {
+ pPlayer->DropFlag();
+ }
+ }
+
+ pPlayer->TeamFortress_SetSpeed();
+ }
+ }
+ }
+
+ for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i )
+ {
+ CBaseObject *pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] );
+ if ( pObj->GetTeamNumber() != iWinningTeam )
+ {
+ // Stop placing any carried objects or else they will float around
+ // at our feet at the end of the round
+ if( pObj->IsPlacing() )
+ {
+ pObj->StopPlacement();
+ }
+
+ // Disable sentry guns that the losing team has built
+ if( pObj->GetType() == OBJ_SENTRYGUN )
+ {
+ pObj->SetDisabled( true );
+ }
+ }
+ }
+
+ if ( m_bForceMapReset )
+ {
+ m_iPrevRoundState = -1;
+ m_iCurrentRoundState = -1;
+ m_iCurrentMiniRoundMask = 0;
+ }
+
+ if ( IsInStopWatch() == true && GetStopWatchTimer() )
+ {
+ variant_t sVariant;
+ GetStopWatchTimer()->AcceptInput( "Pause", NULL, NULL, sVariant, 0 );
+
+ if ( m_bForceMapReset )
+ {
+ if ( GetStopWatchTimer()->IsWatchingTimeStamps() == true )
+ {
+ m_flStopWatchTotalTime = GetStopWatchTimer()->GetStopWatchTotalTime();
+ }
+ else
+ {
+ ShouldResetScores( true, false );
+ UTIL_Remove( m_hStopWatchTimer );
+ m_hStopWatchTimer = NULL;
+ m_flStopWatchTotalTime = -1.0f;
+ m_bStopWatch = false;
+ m_nStopWatchState.Set( STOPWATCH_CAPTURE_TIME_NOT_SET );
+ }
+ }
+ }
+
+#ifdef GAME_DLL
+ if ( GetHalloweenScenario() == HALLOWEEN_SCENARIO_VIADUCT )
+ {
+ // send everyone to the underworld!
+ BroadcastSound( 255, "Halloween.PlayerEscapedUnderworld" );
+
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS );
+ CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS );
+
+ CUtlVector< CBaseEntity * > spawnVector;
+
+ CBaseEntity *spawnPoint = NULL;
+ while( ( spawnPoint = gEntList.FindEntityByClassname( spawnPoint, "info_target" ) ) != NULL )
+ {
+ if ( FStrEq( STRING( spawnPoint->GetEntityName() ), "spawn_warcrimes" ) )
+ {
+ spawnVector.AddToTail( spawnPoint );
+ }
+ }
+
+ if ( spawnVector.Count() > 0 )
+ {
+ // shuffle the order of the spawns
+ int n = spawnVector.Count();
+ while( n > 1 )
+ {
+ int k = RandomInt( 0, n-1 );
+ n--;
+
+ CBaseEntity *tmp = spawnVector[n];
+ spawnVector[n] = spawnVector[k];
+ spawnVector[k] = tmp;
+ }
+
+ color32 fadeColor = { 255, 255, 255, 100 };
+
+ // send players to the underworld
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ CTFPlayer *player = playerVector[i];
+
+ player->SetLocalOrigin( spawnVector[n]->GetAbsOrigin() + Vector( 0, 0, 20.0f ) );
+ player->SetAbsVelocity( vec3_origin );
+ player->SetLocalAngles( spawnVector[n]->GetAbsAngles() );
+ player->m_Local.m_vecPunchAngle = vec3_angle;
+ player->m_Local.m_vecPunchAngleVel = vec3_angle;
+ player->SnapEyeAngles( spawnVector[n]->GetAbsAngles() );
+
+ // give them full health since purgatory damages them over time
+ player->SetHealth( player->GetMaxHealth() );
+
+ UTIL_ScreenFade( player, fadeColor, 0.25, 0.4, FFADE_IN );
+
+ n = ( n + 1 ) % spawnVector.Count();
+ }
+ }
+ }
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Helper function for scramble teams
+//-----------------------------------------------------------------------------
+int FindScoreDifferenceBetweenTeams( CUtlVector< CTFPlayer* > &vecSource, CTFPlayerResource *pPR, int &nRedScore, int &nBlueScore )
+{
+ if ( !pPR )
+ return false;
+
+ nRedScore = 0;
+ nBlueScore = 0;
+
+ FOR_EACH_VEC( vecSource, i )
+ {
+ if ( !vecSource[i] )
+ continue;
+
+ if ( vecSource[i]->GetTeamNumber() == TF_TEAM_RED )
+ {
+ nRedScore += pPR->GetTotalScore( vecSource[i]->entindex() );
+ }
+ else
+ {
+ nBlueScore += pPR->GetTotalScore( vecSource[i]->entindex() );
+ }
+ }
+
+ return abs( nRedScore - nBlueScore );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Helper function for scramble teams
+//-----------------------------------------------------------------------------
+bool FindAndSwapPlayersToBalanceTeams( CUtlVector< CTFPlayer* > &vecSource, int &nDelta, CTFPlayerResource *pPR )
+{
+ if ( !pPR )
+ return false;
+
+ int nTeamScoreRed = 0;
+ int nTeamScoreBlue = 0;
+ FindScoreDifferenceBetweenTeams( vecSource, pPR, nTeamScoreRed, nTeamScoreBlue );
+
+ FOR_EACH_VEC( vecSource, i )
+ {
+ if ( !vecSource[i] )
+ continue;
+
+ if ( vecSource[i]->GetTeamNumber() != TF_TEAM_RED )
+ continue;
+
+ // Check against players on the other team
+ FOR_EACH_VEC( vecSource, j )
+ {
+ if ( !vecSource[j] )
+ continue;
+
+ if ( vecSource[j]->GetTeamNumber() != TF_TEAM_BLUE )
+ continue;
+
+ if ( vecSource[i] == vecSource[j] )
+ continue;
+
+ int nRedPlayerScore = pPR->GetTotalScore( vecSource[i]->entindex() );
+ int nBluePlayerScore = pPR->GetTotalScore( vecSource[j]->entindex() );
+
+ int nPlayerDiff = abs( nRedPlayerScore - nBluePlayerScore );
+ if ( nPlayerDiff )
+ {
+ int nNewRedScore = nTeamScoreRed;
+ int nNewBlueScore = nTeamScoreBlue;
+
+ if ( nRedPlayerScore > nBluePlayerScore )
+ {
+ nNewRedScore -= nPlayerDiff;
+ nNewBlueScore += nPlayerDiff;
+ }
+ else
+ {
+ nNewRedScore += nPlayerDiff;
+ nNewBlueScore -= nPlayerDiff;
+ }
+
+ int nNewDelta = abs( nNewRedScore - nNewBlueScore );
+ if ( nNewDelta < nDelta )
+ {
+ // Swap and recheck
+ vecSource[i]->ForceChangeTeam( TF_TEAM_BLUE );
+ vecSource[j]->ForceChangeTeam( TF_TEAM_RED );
+
+ nDelta = FindScoreDifferenceBetweenTeams( vecSource, pPR, nTeamScoreRed, nTeamScoreBlue );
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::HandleScrambleTeams( void )
+{
+ static CUtlVector< CTFPlayer* > playerVector;
+ playerVector.RemoveAll();
+
+ CollectPlayers( &playerVector, TF_TEAM_RED );
+ CollectPlayers( &playerVector, TF_TEAM_BLUE, false, APPEND_PLAYERS );
+
+ // Sort by player score.
+ playerVector.Sort( ScramblePlayersSort );
+
+ // Put everyone on Spectator to clear the teams (or the autoteam step won't work correctly)
+ FOR_EACH_VEC_BACK( playerVector, i )
+ {
+ if ( !playerVector[i] )
+ {
+ playerVector.Remove( i );
+ continue;
+ }
+ else if ( DuelMiniGame_IsInDuel( playerVector[i] ) ) // don't include them if they're in a duel
+ {
+ playerVector.Remove( i );
+ continue;
+ }
+
+ playerVector[i]->ForceChangeTeam( TEAM_SPECTATOR );
+ }
+
+ // Assign players using the original, quick method.
+ FOR_EACH_VEC( playerVector, i )
+ {
+ if ( !playerVector[i] )
+ continue;
+
+ if ( playerVector[i]->GetTeamNumber() >= FIRST_GAME_TEAM ) // are they already on a game team?
+ continue;
+
+ playerVector[i]->ForceChangeTeam( TF_TEAM_AUTOASSIGN );
+ }
+
+ // New method
+ if ( playerVector.Count() > 2 )
+ {
+ CTFPlayerResource *pPR = dynamic_cast< CTFPlayerResource* >( g_pPlayerResource );
+ if ( pPR )
+ {
+ int nTeamScoreRed = 0;
+ int nTeamScoreBlue = 0;
+ int nDelta = FindScoreDifferenceBetweenTeams( playerVector, pPR, nTeamScoreRed, nTeamScoreBlue );
+
+#ifdef _DEBUG
+ if ( mp_scrambleteams_debug.GetBool() )
+ {
+ DevMsg( "FIRST PASS -- Team1: %i || Team2: %i || Diff: %i\n",
+ nTeamScoreRed,
+ nTeamScoreBlue,
+ nDelta );
+ }
+#endif // _DEBUG
+
+ // Try swapping players to bring scores closer
+ if ( nDelta > 1 )
+ {
+ int nOrigValue = mp_teams_unbalance_limit.GetInt();
+ mp_teams_unbalance_limit.SetValue( 0 );
+
+ static const int nPassLimit = 8;
+ for ( int i = 0; i < nPassLimit && FindAndSwapPlayersToBalanceTeams( playerVector, nDelta, pPR ); ++i )
+ {
+#ifdef _DEBUG
+ if ( mp_scrambleteams_debug.GetBool() )
+ {
+ nTeamScoreRed = 0;
+ nTeamScoreBlue = 0;
+ DevMsg( "EXTRA PASS -- Team1: %i || Team2: %i || Diff: %i\n",
+ nTeamScoreRed,
+ nTeamScoreBlue,
+ FindScoreDifferenceBetweenTeams( playerVector, pPR, nTeamScoreRed, nTeamScoreBlue ) );
+ }
+#endif // _DEBUG
+ }
+
+ mp_teams_unbalance_limit.SetValue( nOrigValue );
+ }
+ }
+ }
+
+ // scrambleteams_auto tracking
+ ResetTeamsRoundWinTracking();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::TeamPlayerCountChanged( CTFTeam *pTeam )
+{
+ if ( m_hGamerulesProxy )
+ {
+ m_hGamerulesProxy->TeamPlayerCountChanged( pTeam );
+ }
+}
+
+
+#ifdef GAME_DLL
+//-----------------------------------------------------------------------------
+// Purpose: Should we attempt to roll into a new match for the current match
+//-----------------------------------------------------------------------------
+bool CTFGameRules::BAttemptMapVoteRollingMatch()
+{
+ if ( IsManagedMatchEnded() )
+ { return false; }
+
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ return pMatchDesc && GTFGCClientSystem()->CanRequestNewMatchForLobby() && pMatchDesc->BUsesMapVoteAfterMatchEnds();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::BIsManagedMatchEndImminent( void )
+{
+/*
+ if ( IsCompetitiveMode() )
+ {
+ if ( State_Get() == GR_STATE_RND_RUNNING )
+ {
+ bool bPotentiallyTheFinalRound = ( CheckWinLimit( false, 1 ) || CheckMaxRounds( false, 1 ) );
+ if ( bPotentiallyTheFinalRound )
+ {
+ bool bPlayingMiniRounds = ( g_hControlPointMasters.Count() && g_hControlPointMasters[0] && g_hControlPointMasters[0]->PlayingMiniRounds() );
+
+ switch( m_nGameType )
+ {
+ case TF_GAMETYPE_ESCORT:
+ {
+ if ( HasMultipleTrains() )
+ {
+
+
+ }
+ else
+ {
+
+
+
+ }
+ }
+ break;
+ case TF_GAMETYPE_CP:
+
+ break;
+ case TF_GAMETYPE_CTF:
+ if ( tf_flag_caps_per_round.GetInt() > 0 )
+ {
+ for ( int iTeam = TF_TEAM_RED; iTeam < TF_TEAM_COUNT; iTeam++ )
+ {
+ C_TFTeam *pTeam = GetGlobalTFTeam( iTeam );
+ if ( pTeam )
+ {
+ if ( ( tf_flag_caps_per_round.GetInt() - pTeam->GetFlagCaptures() ) <= 1 )
+ {
+ CCaptureFlag *pFlag = NULL;
+ for ( int iFlag = 0; iFlag < ICaptureFlagAutoList::AutoList().Count(); ++iFlag )
+ {
+ pFlag = static_cast< CCaptureFlag* >( ICaptureFlagAutoList::AutoList()[iFlag] );
+ if ( !pFlag->IsDisabled() && ( pFlag->GetTeamNumber() == iTeam ) && !pFlag->IsHome() )
+ return true;
+ }
+ }
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ }*/
+
+ return false;
+}
+#endif // GAME_DLL
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::PowerupTeamImbalance( int nTeam )
+{
+ if ( m_hGamerulesProxy )
+ {
+ m_hGamerulesProxy->PowerupTeamImbalance( nTeam );
+
+ if ( nTeam == TEAM_UNASSIGNED )
+ {
+ m_bPowerupImbalanceMeasuresRunning = false;
+ }
+ else
+ {
+ m_bPowerupImbalanceMeasuresRunning = true;
+ m_flTimeToStopImbalanceMeasures = gpGlobals->curtime + m_flTimeToRunImbalanceMeasures;
+
+ BroadcastSound( nTeam, "Announcer.Powerup.Volume.Starting" );
+ CTeamRecipientFilter filter( nTeam, true );
+ UTIL_ClientPrintFilter( filter, HUD_PRINTCENTER, "#TF_Powerupvolume_Available" );
+ }
+
+ m_nPowerupKillsBlueTeam = 0; // Reset both scores
+ m_nPowerupKillsRedTeam = 0;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Restrict team human players can join
+//-----------------------------------------------------------------------------
+int CTFGameRules::GetAssignedHumanTeam( void )
+{
+ if ( FStrEq( "blue", mp_humans_must_join_team.GetString() ) )
+ {
+ return TF_TEAM_BLUE;
+ }
+ else if ( FStrEq( "red", mp_humans_must_join_team.GetString() ) )
+ {
+ return TF_TEAM_RED;
+ }
+ else if ( FStrEq( "spectator", mp_humans_must_join_team.GetString() ) )
+ {
+ return TEAM_SPECTATOR;
+ }
+ else
+ {
+ return TEAM_ANY;
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::HandleSwitchTeams( void )
+{
+ if ( IsPVEModeActive() )
+ return;
+
+ m_bTeamsSwitched.Set( !m_bTeamsSwitched );
+
+ // switch this as well
+ if ( FStrEq( mp_humans_must_join_team.GetString(), "blue" ) )
+ {
+ mp_humans_must_join_team.SetValue( "red" );
+ }
+ else if ( FStrEq( mp_humans_must_join_team.GetString(), "red" ) )
+ {
+ mp_humans_must_join_team.SetValue( "blue" );
+ }
+
+ int i = 0;
+
+ // remove everyone's projectiles and objects
+ RemoveAllProjectilesAndBuildings();
+
+ // respawn the players
+ for ( i = 1 ; i <= gpGlobals->maxClients ; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( pPlayer )
+ {
+ if ( pPlayer->GetTeamNumber() == TF_TEAM_RED )
+ {
+ pPlayer->ForceChangeTeam( TF_TEAM_BLUE, true );
+ }
+ else if ( pPlayer->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ pPlayer->ForceChangeTeam( TF_TEAM_RED, true );
+ }
+ }
+ }
+
+ // switch the team scores
+ CTFTeam *pRedTeam = GetGlobalTFTeam( TF_TEAM_RED );
+ CTFTeam *pBlueTeam = GetGlobalTFTeam( TF_TEAM_BLUE );
+ if ( pRedTeam && pBlueTeam )
+ {
+ int nRed = pRedTeam->GetScore();
+ int nBlue = pBlueTeam->GetScore();
+
+ pRedTeam->SetScore( nBlue );
+ pBlueTeam->SetScore( nRed );
+
+ if ( IsInTournamentMode() == true )
+ {
+ char szBlueName[16];
+ char szRedName[16];
+
+ Q_strncpy( szBlueName, mp_tournament_blueteamname.GetString(), sizeof ( szBlueName ) );
+ Q_strncpy( szRedName, mp_tournament_redteamname.GetString(), sizeof ( szRedName ) );
+
+ mp_tournament_redteamname.SetValue( szBlueName );
+ mp_tournament_blueteamname.SetValue( szRedName );
+ }
+ }
+
+ UTIL_ClientPrintAll( HUD_PRINTTALK, "#TF_TeamsSwitched" );
+
+ CMatchInfo *pMatchInfo = GTFGCClientSystem()->GetMatch();
+ if ( pMatchInfo )
+ {
+ CTFPlayerResource *pTFResource = dynamic_cast< CTFPlayerResource* >( g_pPlayerResource );
+ if ( pTFResource )
+ {
+ uint32 unEventTeamStatus = pTFResource->GetEventTeamStatus();
+
+ if ( unEventTeamStatus && m_bTeamsSwitched )
+ {
+ const uint32 unInvadersArePyro = 1u;
+ const uint32 unInvadersAreHeavy = 2u;
+ unEventTeamStatus = ( unEventTeamStatus == unInvadersArePyro ) ? unInvadersAreHeavy : unInvadersArePyro;
+ }
+
+ pMatchInfo->m_unEventTeamStatus = unEventTeamStatus;
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CanChangeClassInStalemate( void )
+{
+ return (gpGlobals->curtime < (m_flStalemateStartTime + tf_stalematechangeclasstime.GetFloat()));
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CanChangeTeam( int iCurrentTeam ) const
+{
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ if ( ( iCurrentTeam == TF_TEAM_RED ) || ( iCurrentTeam == TF_TEAM_BLUE ) )
+ {
+ return !ArePlayersInHell();
+ }
+ }
+
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetRoundOverlayDetails( void )
+{
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+
+ if ( pMaster && pMaster->PlayingMiniRounds() )
+ {
+ CTeamControlPointRound *pRound = pMaster->GetCurrentRound();
+
+ if ( pRound )
+ {
+ CHandle<CTeamControlPoint> pRedPoint = pRound->GetPointOwnedBy( TF_TEAM_RED );
+ CHandle<CTeamControlPoint> pBluePoint = pRound->GetPointOwnedBy( TF_TEAM_BLUE );
+
+ // do we have opposing points in this round?
+ if ( pRedPoint && pBluePoint )
+ {
+ int iMiniRoundMask = ( 1<<pBluePoint->GetPointIndex() ) | ( 1<<pRedPoint->GetPointIndex() );
+ SetMiniRoundBitMask( iMiniRoundMask );
+ }
+ else
+ {
+ SetMiniRoundBitMask( 0 );
+ }
+
+ SetCurrentRoundStateBitString();
+ }
+ }
+
+ BaseClass::SetRoundOverlayDetails();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Returns whether a team should score for each captured point
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldScorePerRound( void )
+{
+ bool bRetVal = true;
+
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( pMaster && pMaster->ShouldScorePerCapture() )
+ {
+ bRetVal = false;
+ }
+
+ return bRetVal;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsValveMap( void )
+{
+ char szCurrentMap[MAX_MAP_NAME];
+ Q_strncpy( szCurrentMap, STRING( gpGlobals->mapname ), sizeof( szCurrentMap ) );
+
+ if ( ::IsValveMap( szCurrentMap ) )
+ {
+ return true;
+ }
+
+ return BaseClass::IsValveMap();
+}
+
+void CTFGameRules::PlayTrainCaptureAlert( CTeamControlPoint *pPoint, bool bFinalPointInMap )
+{
+ if ( !pPoint )
+ return;
+
+ if ( State_Get() != GR_STATE_RND_RUNNING )
+ return;
+
+ const char *pszAlert = TEAM_TRAIN_ALERT;
+
+ // is this the final control point in the map?
+ if ( bFinalPointInMap )
+ {
+ pszAlert = TEAM_TRAIN_FINAL_ALERT;
+ }
+
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ if ( bFinalPointInMap )
+ {
+ int iWinningTeam = TEAM_UNASSIGNED;
+ float flRedProgress = 0.0f, flBlueProgress = 0.0f;
+ for ( int i = 0 ; i < ITFTeamTrainWatcher::AutoList().Count() ; ++i )
+ {
+ CTeamTrainWatcher *pTrainWatcher = static_cast< CTeamTrainWatcher* >( ITFTeamTrainWatcher::AutoList()[i] );
+ if ( !pTrainWatcher->IsDisabled() )
+ {
+ if ( pTrainWatcher->GetTeamNumber() == TF_TEAM_RED )
+ {
+ flRedProgress = pTrainWatcher->GetTrainDistanceAlongTrack();
+ }
+ else
+ {
+ flBlueProgress = pTrainWatcher->GetTrainDistanceAlongTrack();
+ }
+ }
+ }
+
+ if ( flRedProgress > flBlueProgress )
+ {
+ iWinningTeam = TF_TEAM_RED;
+ }
+ else if ( flBlueProgress > flRedProgress )
+ {
+ iWinningTeam = TF_TEAM_BLUE;
+ }
+
+ if ( iWinningTeam != TEAM_UNASSIGNED )
+ {
+ int iRedLine, iBlueLine;
+ iRedLine = ( iWinningTeam == TF_TEAM_RED ) ? HELLTOWER_VO_RED_NEAR_WIN : HELLTOWER_VO_RED_NEAR_LOSE;
+ iBlueLine = ( iWinningTeam == TF_TEAM_BLUE ) ? HELLTOWER_VO_BLUE_NEAR_WIN : HELLTOWER_VO_BLUE_NEAR_LOSE;
+ PlayHelltowerAnnouncerVO( iRedLine, iBlueLine );
+ }
+ }
+ return;
+ }
+
+ CBroadcastRecipientFilter filter;
+ pPoint->EmitSound( filter, pPoint->entindex(), pszAlert );
+}
+
+#endif // GAME_DLL
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CTFGameRules::GetFarthestOwnedControlPoint( int iTeam, bool bWithSpawnpoints )
+{
+ int iOwnedEnd = ObjectiveResource()->GetBaseControlPointForTeam( iTeam );
+ if ( iOwnedEnd == -1 )
+ return -1;
+
+ int iNumControlPoints = ObjectiveResource()->GetNumControlPoints();
+ int iWalk = 1;
+ int iEnemyEnd = iNumControlPoints-1;
+ if ( iOwnedEnd != 0 )
+ {
+ iWalk = -1;
+ iEnemyEnd = 0;
+ }
+
+ // Walk towards the other side, and find the farthest owned point that has spawn points
+ int iFarthestPoint = iOwnedEnd;
+ for ( int iPoint = iOwnedEnd; iPoint != iEnemyEnd; iPoint += iWalk )
+ {
+ // If we've hit a point we don't own, we're done
+ if ( ObjectiveResource()->GetOwningTeam( iPoint ) != iTeam )
+ break;
+
+ if ( bWithSpawnpoints && !m_bControlSpawnsPerTeam[iTeam][iPoint] )
+ continue;
+
+ iFarthestPoint = iPoint;
+ }
+
+ return iFarthestPoint;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::TeamMayCapturePoint( int iTeam, int iPointIndex )
+{
+ if ( !tf_caplinear.GetBool() )
+ return true;
+
+ // Any previous points necessary?
+ int iPointNeeded = ObjectiveResource()->GetPreviousPointForPoint( iPointIndex, iTeam, 0 );
+
+ // Points set to require themselves are always cappable
+ if ( iPointNeeded == iPointIndex )
+ return true;
+
+ if ( IsInKothMode() && IsInWaitingForPlayers() )
+ return false;
+
+ // Is the point locked?
+ if ( ObjectiveResource()->GetCPLocked( iPointIndex ) )
+ return false;
+
+ // No required points specified? Require all previous points.
+ if ( iPointNeeded == -1 )
+ {
+ if ( IsInArenaMode() == true )
+ {
+
+#ifdef CLIENT_DLL
+
+ if ( m_flCapturePointEnableTime - 5.0f <= gpGlobals->curtime && State_Get() == GR_STATE_STALEMATE )
+ return true;
+#endif
+
+ if ( m_flCapturePointEnableTime <= gpGlobals->curtime && State_Get() == GR_STATE_STALEMATE )
+ return true;
+
+ return false;
+ }
+
+ if ( !ObjectiveResource()->PlayingMiniRounds() )
+ {
+ // No custom previous point, team must own all previous points
+ int iFarthestPoint = GetFarthestOwnedControlPoint( iTeam, false );
+ return (abs(iFarthestPoint - iPointIndex) <= 1);
+ }
+ else
+ {
+ // No custom previous point, team must own all previous points in the current mini-round
+ //tagES TFTODO: need to figure out a good algorithm for this
+ return true;
+ }
+ }
+
+ // Loop through each previous point and see if the team owns it
+ for ( int iPrevPoint = 0; iPrevPoint < MAX_PREVIOUS_POINTS; iPrevPoint++ )
+ {
+ iPointNeeded = ObjectiveResource()->GetPreviousPointForPoint( iPointIndex, iTeam, iPrevPoint );
+ if ( iPointNeeded != -1 )
+ {
+ if ( ObjectiveResource()->GetOwningTeam( iPointNeeded ) != iTeam )
+ return false;
+ }
+ }
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::PlayerMayCapturePoint( CBasePlayer *pPlayer, int iPointIndex, char *pszReason /* = NULL */, int iMaxReasonLength /* = 0 */ )
+{
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+
+ if ( !pTFPlayer )
+ {
+ return false;
+ }
+
+ // Disguised and invisible spies cannot capture points
+ if ( pTFPlayer->m_Shared.IsStealthed() )
+ {
+ if ( pszReason )
+ {
+ Q_snprintf( pszReason, iMaxReasonLength, "#Cant_cap_stealthed" );
+ }
+ return false;
+ }
+
+ if ( ( pTFPlayer->m_Shared.IsInvulnerable() || pTFPlayer->m_Shared.InCond( TF_COND_MEGAHEAL ) ) && !pTFPlayer->m_bInPowerPlay && !IsMannVsMachineMode() )
+ {
+ if ( pszReason )
+ {
+ Q_snprintf( pszReason, iMaxReasonLength, "#Cant_cap_invuln" );
+ }
+ return false;
+ }
+
+ if ( pTFPlayer->m_Shared.InCond( TF_COND_PHASE ) )
+ {
+ if ( pszReason )
+ {
+ Q_snprintf( pszReason, iMaxReasonLength, "#Cant_cap_invuln" );
+ }
+
+ return false;
+ }
+
+ if ( pTFPlayer->m_Shared.IsControlStunned() )
+ {
+ if ( pszReason )
+ {
+ Q_snprintf( pszReason, iMaxReasonLength, "#Cant_cap_stunned" );
+ }
+
+ return false;
+ }
+
+ // spies disguised as the enemy team cannot capture points
+ if ( pTFPlayer->m_Shared.InCond( TF_COND_DISGUISED ) && pTFPlayer->m_Shared.GetDisguiseTeam() != pTFPlayer->GetTeamNumber() )
+ {
+ if ( pszReason )
+ {
+ Q_snprintf( pszReason, iMaxReasonLength, "#Cant_cap_disguised" );
+ }
+ return false;
+ }
+
+#ifdef GAME_DLL
+ if ( IsInTraining() && pTFPlayer->IsBotOfType( TF_BOT_TYPE ) )
+ {
+ switch( GetGameType() )
+ {
+ case TF_GAMETYPE_CP:
+ {
+ // in training mode, bots cannot initiate a capture
+ float flCapPerc = ObjectiveResource()->GetCPCapPercentage( iPointIndex );
+ if ( flCapPerc <= 0.0f )
+ {
+ return false;
+ }
+ break;
+ }
+
+ case TF_GAMETYPE_ESCORT:
+ {
+ // in training mode, the player must push the cart for the first time
+ // after that, bots can start it moving again
+
+ // assume only one cart in the map
+ CTeamTrainWatcher *watcher = NULL;
+ while( ( watcher = dynamic_cast< CTeamTrainWatcher * >( gEntList.FindEntityByClassname( watcher, "team_train_watcher" ) ) ) != NULL )
+ {
+ if ( !watcher->IsDisabled() )
+ {
+ break;
+ }
+ }
+
+ if ( watcher && !watcher->IsDisabled() )
+ {
+ return !watcher->IsTrainAtStart();
+ }
+ break;
+ }
+ }
+ }
+
+#ifdef TF_CREEP_MODE
+ if ( IsCreepWaveMode() )
+ {
+ CTFBot *bot = ToTFBot( pTFPlayer );
+
+ if ( !bot || !bot->HasAttribute( CTFBot::IS_NPC ) )
+ {
+ // only creeps can capture points
+ return false;
+ }
+ }
+#endif
+
+#endif
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::PlayerMayBlockPoint( CBasePlayer *pPlayer, int iPointIndex, char *pszReason, int iMaxReasonLength )
+{
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+ if ( !pTFPlayer )
+ return false;
+
+#ifdef GAME_DLL
+#ifdef TF_CREEP_MODE
+ if ( IsCreepWaveMode() )
+ {
+ CTFBot *bot = ToTFBot( pTFPlayer );
+
+ if ( !bot || !bot->HasAttribute( CTFBot::IS_NPC ) )
+ {
+ // only creeps can block points
+ return false;
+ }
+ }
+#endif
+#endif
+
+ // Invuln players can block points
+ if ( pTFPlayer->m_Shared.IsInvulnerable() )
+ {
+ if ( pszReason )
+ {
+ Q_snprintf( pszReason, iMaxReasonLength, "#Cant_cap_invuln" );
+ }
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Calculates score for player
+//-----------------------------------------------------------------------------
+int CTFGameRules::CalcPlayerScore( RoundStats_t *pRoundStats, CTFPlayer *pPlayer )
+{
+ Assert( pRoundStats );
+ if ( !pRoundStats )
+ return 0;
+
+ // defensive fix for the moment for bug where healing value becomes bogus sometimes: if bogus, slam it to 0
+ int iHealing = pRoundStats->m_iStat[TFSTAT_HEALING];
+ Assert( iHealing >= 0 );
+ Assert( iHealing <= 10000000 );
+ if ( iHealing < 0 || iHealing > 10000000 )
+ {
+ iHealing = 0;
+ }
+
+ bool bMvM = TFGameRules() && TFGameRules()->IsMannVsMachineMode();
+ bool bPowerupMode = TFGameRules() && TFGameRules()->IsPowerupMode();
+
+ int iScore = ( pRoundStats->m_iStat[TFSTAT_KILLS] * TF_SCORE_KILL ) +
+ ( pRoundStats->m_iStat[TFSTAT_KILLS_RUNECARRIER] * TF_SCORE_KILL_RUNECARRIER ) + // Kill someone who is carrying a rune
+ ( pRoundStats->m_iStat[TFSTAT_CAPTURES] * ( ( bPowerupMode ) ? TF_SCORE_CAPTURE_POWERUPMODE : TF_SCORE_CAPTURE ) ) +
+ ( pRoundStats->m_iStat[TFSTAT_FLAGRETURNS] * TF_SCORE_FLAG_RETURN ) +
+ ( pRoundStats->m_iStat[TFSTAT_DEFENSES] * TF_SCORE_DEFEND ) +
+ ( pRoundStats->m_iStat[TFSTAT_BUILDINGSDESTROYED] * TF_SCORE_DESTROY_BUILDING ) +
+ ( pRoundStats->m_iStat[TFSTAT_HEADSHOTS] / TF_SCORE_HEADSHOT_DIVISOR ) +
+ ( pRoundStats->m_iStat[TFSTAT_BACKSTABS] * TF_SCORE_BACKSTAB ) +
+ ( iHealing / ( ( bMvM ) ? TF_SCORE_DAMAGE : TF_SCORE_HEAL_HEALTHUNITS_PER_POINT ) ) + // MvM values healing more than PvP
+ ( pRoundStats->m_iStat[TFSTAT_KILLASSISTS] / TF_SCORE_KILL_ASSISTS_PER_POINT ) +
+ ( pRoundStats->m_iStat[TFSTAT_TELEPORTS] / TF_SCORE_TELEPORTS_PER_POINT ) +
+ ( pRoundStats->m_iStat[TFSTAT_INVULNS] / TF_SCORE_INVULN ) +
+ ( pRoundStats->m_iStat[TFSTAT_REVENGE] / TF_SCORE_REVENGE ) +
+ ( pRoundStats->m_iStat[TFSTAT_BONUS_POINTS] / TF_SCORE_BONUS_POINT_DIVISOR );
+ ( pRoundStats->m_iStat[TFSTAT_CURRENCY_COLLECTED] / TF_SCORE_CURRENCY_COLLECTED );
+
+ if ( pPlayer )
+ {
+ int nScoreboardMinigame = 0;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pPlayer, nScoreboardMinigame, scoreboard_minigame );
+ if ( nScoreboardMinigame > 0 )
+ {
+ // Increment Score
+ iScore +=
+ ( pRoundStats->m_iStat[TFSTAT_KILLS] ) +
+ ( pRoundStats->m_iStat[TFSTAT_CAPTURES] ) +
+ ( pRoundStats->m_iStat[TFSTAT_DEFENSES] ) +
+ ( pRoundStats->m_iStat[TFSTAT_BUILDINGSDESTROYED] );
+
+ // Subtract Deaths
+ iScore -= pRoundStats->m_iStat[TFSTAT_DEATHS] * 3;
+ }
+ }
+
+ // Previously MvM-only
+ const int nDivisor = ( bMvM ) ? TF_SCORE_DAMAGE : TF_SCORE_HEAL_HEALTHUNITS_PER_POINT;
+ iScore += ( pRoundStats->m_iStat[TFSTAT_DAMAGE] / nDivisor );
+ iScore += ( pRoundStats->m_iStat[TFSTAT_DAMAGE_ASSIST] / nDivisor );
+ iScore += ( pRoundStats->m_iStat[TFSTAT_DAMAGE_BOSS] / nDivisor );
+ iScore += ( pRoundStats->m_iStat[TFSTAT_HEALING_ASSIST] / nDivisor );
+ iScore += ( pRoundStats->m_iStat[TFSTAT_DAMAGE_BLOCKED] / nDivisor );
+
+ return Max( iScore, 0 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Calculates score for player
+//-----------------------------------------------------------------------------
+int CTFGameRules::CalcPlayerSupportScore( RoundStats_t *pRoundStats, int iPlayerIdx )
+{
+#ifdef GAME_DLL
+ Assert( pRoundStats );
+ if ( !pRoundStats )
+ return 0;
+
+ return ( pRoundStats->m_iStat[TFSTAT_DAMAGE_ASSIST] +
+ pRoundStats->m_iStat[TFSTAT_HEALING_ASSIST] +
+ pRoundStats->m_iStat[TFSTAT_DAMAGE_BLOCKED] +
+ ( pRoundStats->m_iStat[TFSTAT_BONUS_POINTS] * 25 ) );
+#else
+ Assert( g_TF_PR );
+ if ( !g_TF_PR )
+ return 0;
+
+ return g_TF_PR->GetDamageAssist( iPlayerIdx ) +
+ g_TF_PR->GetHealingAssist( iPlayerIdx ) +
+ g_TF_PR->GetDamageBlocked( iPlayerIdx ) +
+ ( g_TF_PR->GetBonusPoints( iPlayerIdx ) * 25 );
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsBirthday( void ) const
+{
+ if ( IsX360() )
+ return false;
+
+ return tf_birthday.GetBool() || IsHolidayActive( kHoliday_TFBirthday );
+}
+
+bool CTFGameRules::IsBirthdayOrPyroVision( void ) const
+{
+ if ( IsBirthday() )
+ return true;
+
+#ifdef CLIENT_DLL
+ // Use birthday fun if the local player has an item that allows them to see it (Pyro Goggles)
+ if ( IsLocalPlayerUsingVisionFilterFlags( TF_VISION_FILTER_PYRO ) )
+ {
+ return true;
+ }
+#endif
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsHolidayActive( /*EHoliday*/ int eHoliday ) const
+{
+ //if ( IsPVEModeActive() )
+ // return false;
+
+ return TF_IsHolidayActive( eHoliday );
+}
+
+#ifndef GAME_DLL
+void CTFGameRules::SetUpVisionFilterKeyValues( void )
+{
+ m_pkvVisionFilterShadersMapWhitelist = new KeyValues( "VisionFilterShadersMapWhitelist" );
+ m_pkvVisionFilterShadersMapWhitelist->LoadFromFile( g_pFullFileSystem, "cfg/mtp.cfg", "MOD" );
+
+ m_pkvVisionFilterTranslations = new KeyValues( "VisionFilterTranslations" );
+
+ // **************************************************************************************************
+ // PARTICLES
+ KeyValues *pKVBlock = new KeyValues( "particles" );
+ m_pkvVisionFilterTranslations->AddSubKey( pKVBlock );
+
+ // No special vision
+ KeyValues *pKVFlag = new KeyValues( "0" );
+ pKVFlag->SetString( "flamethrower_rainbow", "flamethrower" );
+ pKVFlag->SetString( "flamethrower_rainbow_FP", "flamethrower" );
+ pKVBlock->AddSubKey( pKVFlag );
+
+ // Pyrovision
+ pKVFlag = new KeyValues( "1" ); //TF_VISION_FILTER_PYRO
+ pKVFlag->SetString( "flamethrower_rainbow", "flamethrower_rainbow" ); // Rainblower defaults to rainbows and we want to ensure that we use it
+ pKVFlag->SetString( "flamethrower_rainbow_FP", "flamethrower_rainbow_FP" );
+ pKVFlag->SetString( "burningplayer_blue", "burningplayer_rainbow_blue" );
+ pKVFlag->SetString( "burningplayer_red", "burningplayer_rainbow_red" );
+ pKVFlag->SetString( "burningplayer_corpse", "burningplayer_corpse_rainbow" );
+ pKVFlag->SetString( "water_blood_impact_red_01", "pyrovision_blood" );
+ pKVFlag->SetString( "blood_impact_red_01", "pyrovision_blood" );
+ pKVFlag->SetString( "blood_spray_red_01", "pyrovision_blood" );
+ pKVFlag->SetString( "blood_spray_red_01_far", "pyrovision_blood" );
+ pKVFlag->SetString( "ExplosionCore_wall", "pyrovision_explosion" );
+ pKVFlag->SetString( "ExplosionCore_buildings", "pyrovision_explosion" );
+ pKVFlag->SetString( "ExplosionCore_MidAir", "pyrovision_explosion" );
+ pKVFlag->SetString( "rockettrail", "pyrovision_rockettrail" );
+ pKVFlag->SetString( "rockettrail_!", "pyrovision_rockettrail" );
+ pKVFlag->SetString( "sentry_rocket", "pyrovision_rockettrail" );
+ pKVFlag->SetString( "flaregun_trail_blue", "pyrovision_flaregun_trail_blue" );
+ pKVFlag->SetString( "flaregun_trail_red", "pyrovision_flaregun_trail_red" );
+ pKVFlag->SetString( "flaregun_trail_crit_blue", "pyrovision_flaregun_trail_crit_blue" );
+ pKVFlag->SetString( "flaregun_trail_crit_red", "pyrovision_flaregun_trail_crit_red" );
+ pKVFlag->SetString( "flaregun_destroyed", "pyrovision_flaregun_destroyed" );
+ pKVFlag->SetString( "scorchshot_trail_blue", "pyrovision_scorchshot_trail_blue" );
+ pKVFlag->SetString( "scorchshot_trail_red", "pyrovision_scorchshot_trail_red" );
+ pKVFlag->SetString( "scorchshot_trail_crit_blue", "pyrovision_scorchshot_trail_crit_blue" );
+ pKVFlag->SetString( "scorchshot_trail_crit_red", "pyrovision_scorchshot_trail_crit_red" );
+ pKVFlag->SetString( "ExplosionCore_MidAir_Flare", "pyrovision_flaregun_destroyed" );
+ pKVFlag->SetString( "pyrotaunt_rainbow_norainbow", "pyrotaunt_rainbow" );
+ pKVFlag->SetString( "pyrotaunt_rainbow_bubbles_flame", "pyrotaunt_rainbow_bubbles" );
+ pKVFlag->SetString( "v_flaming_arrow", "pyrovision_v_flaming_arrow" );
+ pKVFlag->SetString( "flaming_arrow", "pyrovision_flaming_arrow" );
+ pKVFlag->SetString( "flying_flaming_arrow", "pyrovision_flying_flaming_arrow" );
+ pKVFlag->SetString( "taunt_pyro_balloon", "taunt_pyro_balloon_vision" );
+ pKVFlag->SetString( "taunt_pyro_balloon_explosion", "taunt_pyro_balloon_explosion_vision" );
+
+ pKVBlock->AddSubKey( pKVFlag );
+
+ //// Spooky Vision
+ //pKVFlag = new KeyValues( "2" ); //TF_VISION_FILTER_HALLOWEEN
+ //pKVBlock->AddSubKey( pKVFlag );
+
+ // **************************************************************************************************
+ // SOUNDS
+ pKVBlock = new KeyValues( "sounds" );
+ m_pkvVisionFilterTranslations->AddSubKey( pKVBlock );
+
+ // No special vision
+ pKVFlag = new KeyValues( "0" );
+ pKVFlag->SetString( "weapons/rainblower/rainblower_start.wav", "weapons/flame_thrower_start.wav" );
+ pKVFlag->SetString( "weapons/rainblower/rainblower_loop.wav", "weapons/flame_thrower_loop.wav" );
+ pKVFlag->SetString( "weapons/rainblower/rainblower_end.wav", "weapons/flame_thrower_end.wav" );
+ pKVFlag->SetString( "weapons/rainblower/rainblower_hit.wav", "weapons/flame_thrower_fire_hit.wav" );
+ pKVFlag->SetString( "weapons/rainblower/rainblower_pilot.wav", "weapons/flame_thrower_pilot.wav" );
+ pKVFlag->SetString( ")weapons/rainblower/rainblower_start.wav", ")weapons/flame_thrower_start.wav" );
+ pKVFlag->SetString( ")weapons/rainblower/rainblower_loop.wav", ")weapons/flame_thrower_loop.wav" );
+ pKVFlag->SetString( ")weapons/rainblower/rainblower_end.wav", ")weapons/flame_thrower_end.wav" );
+ pKVFlag->SetString( ")weapons/rainblower/rainblower_hit.wav", ")weapons/flame_thrower_fire_hit.wav" );
+ pKVFlag->SetString( ")weapons/rainblower/rainblower_pilot.wav", "weapons/flame_thrower_pilot.wav" );
+ pKVFlag->SetString( "Weapon_Rainblower.Fire", "Weapon_FlameThrower.Fire" );
+ pKVFlag->SetString( "Weapon_Rainblower.FireLoop", "Weapon_FlameThrower.FireLoop" );
+ pKVFlag->SetString( "Weapon_Rainblower.WindDown", "Weapon_FlameThrower.WindDown" );
+ pKVFlag->SetString( "Weapon_Rainblower.FireHit", "Weapon_FlameThrower.FireHit" );
+ pKVFlag->SetString( "Weapon_Rainblower.PilotLoop", "Weapon_FlameThrower.PilotLoop" );
+ pKVFlag->SetString( "Taunt.PyroBalloonicorn", "Taunt.PyroHellicorn" );
+ pKVFlag->SetString( ")items/pyro_music_tube.wav", "common/null.wav" );
+ pKVBlock->AddSubKey( pKVFlag );
+
+ // Pyrovision
+ pKVFlag = new KeyValues( "1" ); //TF_VISION_FILTER_PYRO
+ pKVFlag->SetString( "weapons/rainblower/rainblower_start.wav", "weapons/rainblower/rainblower_start.wav" ); // If we're in PyroVision, explicitly set to ensure this is saved
+ pKVFlag->SetString( "weapons/rainblower/rainblower_loop.wav", "weapons/rainblower/rainblower_loop.wav" );
+ pKVFlag->SetString( "weapons/rainblower/rainblower_end.wav", "weapons/rainblower/rainblower_end.wav" );
+ pKVFlag->SetString( "weapons/rainblower/rainblower_hit.wav", "weapons/rainblower/rainblower_hit.wav" );
+ pKVFlag->SetString( "weapons/rainblower/rainblower_pilot.wav", "weapons/rainblower/rainblower_pilot.wav" );
+ pKVFlag->SetString( ")weapons/rainblower/rainblower_start.wav", ")weapons/rainblower/rainblower_start.wav" );
+ pKVFlag->SetString( ")weapons/rainblower/rainblower_loop.wav", ")weapons/rainblower/rainblower_loop.wav" );
+ pKVFlag->SetString( ")weapons/rainblower/rainblower_end.wav", ")weapons/rainblower/rainblower_end.wav" );
+ pKVFlag->SetString( ")weapons/rainblower/rainblower_hit.wav", ")weapons/rainblower/rainblower_hit.wav" );
+ pKVFlag->SetString( ")weapons/rainblower/rainblower_pilot.wav", ")weapons/rainblower/rainblower_pilot.wav" );
+ pKVFlag->SetString( "Weapon_Rainblower.Fire", "Weapon_Rainblower.Fire" );
+ pKVFlag->SetString( "Weapon_Rainblower.FireLoop", "Weapon_Rainblower.FireLoop" );
+ pKVFlag->SetString( "Weapon_Rainblower.WindDown", "Weapon_Rainblower.WindDown" );
+ pKVFlag->SetString( "Weapon_Rainblower.FireHit", "Weapon_Rainblower.FireHit" );
+ pKVFlag->SetString( "Weapon_Rainblower.PilotLoop", "Weapon_Rainblower.PilotLoop" );
+ pKVFlag->SetString( "Taunt.PyroBalloonicorn", "Taunt.PyroBalloonicorn" );
+ pKVFlag->SetString( ")items/pyro_music_tube.wav", ")items/pyro_music_tube.wav" );
+
+ pKVFlag->SetString( "vo/demoman_PainCrticialDeath01.mp3", "vo/demoman_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainCrticialDeath02.mp3", "vo/demoman_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainCrticialDeath03.mp3", "vo/demoman_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainCrticialDeath04.mp3", "vo/demoman_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainCrticialDeath05.mp3", "vo/demoman_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainSevere01.mp3", "vo/demoman_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainSevere02.mp3", "vo/demoman_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainSevere03.mp3", "vo/demoman_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainSevere04.mp3", "vo/demoman_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainSharp01.mp3", "vo/demoman_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainSharp02.mp3", "vo/demoman_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainSharp03.mp3", "vo/demoman_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainSharp04.mp3", "vo/demoman_LaughShort04.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainSharp05.mp3", "vo/demoman_LaughShort05.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainSharp06.mp3", "vo/demoman_LaughShort06.mp3" );
+ pKVFlag->SetString( "vo/demoman_PainSharp07.mp3", "vo/demoman_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/demoman_AutoOnFire01.mp3", "vo/demoman_PositiveVocalization02.mp3" );
+ pKVFlag->SetString( "vo/demoman_AutoOnFire02.mp3", "vo/demoman_PositiveVocalization03.mp3" );
+ pKVFlag->SetString( "vo/demoman_AutoOnFire03.mp3", "vo/demoman_PositiveVocalization04.mp3" );
+
+ pKVFlag->SetString( "vo/engineer_PainCrticialDeath01.mp3", "vo/engineer_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainCrticialDeath02.mp3", "vo/engineer_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainCrticialDeath03.mp3", "vo/engineer_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainCrticialDeath04.mp3", "vo/engineer_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainCrticialDeath05.mp3", "vo/engineer_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainCrticialDeath06.mp3", "vo/engineer_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSevere01.mp3", "vo/engineer_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSevere02.mp3", "vo/engineer_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSevere03.mp3", "vo/engineer_LaughHappy03.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSevere04.mp3", "vo/engineer_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSevere05.mp3", "vo/engineer_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSevere06.mp3", "vo/engineer_LaughHappy03.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSevere07.mp3", "vo/engineer_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSharp01.mp3", "vo/engineer_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSharp02.mp3", "vo/engineer_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSharp03.mp3", "vo/engineer_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSharp04.mp3", "vo/engineer_LaughShort04.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSharp05.mp3", "vo/engineer_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSharp06.mp3", "vo/engineer_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSharp07.mp3", "vo/engineer_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/engineer_PainSharp08.mp3", "vo/engineer_LaughShort04.mp3" );
+ pKVFlag->SetString( "vo/engineer_AutoOnFire01.mp3", "vo/engineer_PositiveVocalization01.mp3" );
+ pKVFlag->SetString( "vo/engineer_AutoOnFire02.mp3", "vo/engineer_Cheers01.mp3" );
+ pKVFlag->SetString( "vo/engineer_AutoOnFire03.mp3", "vo/engineer_Cheers02.mp3" );
+
+ pKVFlag->SetString( "vo/heavy_PainCrticialDeath01.mp3", "vo/heavy_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/heavy_PainCrticialDeath02.mp3", "vo/heavy_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/heavy_PainCrticialDeath03.mp3", "vo/heavy_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/heavy_PainSevere01.mp3", "vo/heavy_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/heavy_PainSevere02.mp3", "vo/heavy_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/heavy_PainSevere03.mp3", "vo/heavy_LaughHappy03.mp3" );
+ pKVFlag->SetString( "vo/heavy_PainSharp01.mp3", "vo/heavy_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/heavy_PainSharp02.mp3", "vo/heavy_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/heavy_PainSharp03.mp3", "vo/heavy_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/heavy_PainSharp04.mp3", "vo/heavy_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/heavy_PainSharp05.mp3", "vo/heavy_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/heavy_AutoOnFire01.mp3", "vo/heavy_PositiveVocalization01.mp3" );
+ pKVFlag->SetString( "vo/heavy_AutoOnFire02.mp3", "vo/heavy_PositiveVocalization02.mp3" );
+ pKVFlag->SetString( "vo/heavy_AutoOnFire03.mp3", "vo/heavy_PositiveVocalization03.mp3" );
+ pKVFlag->SetString( "vo/heavy_AutoOnFire04.mp3", "vo/heavy_PositiveVocalization04.mp3" );
+ pKVFlag->SetString( "vo/heavy_AutoOnFire05.mp3", "vo/heavy_PositiveVocalization05.mp3" );
+
+ pKVFlag->SetString( "vo/medic_PainCrticialDeath01.mp3", "vo/medic_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/medic_PainCrticialDeath02.mp3", "vo/medic_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/medic_PainCrticialDeath03.mp3", "vo/medic_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/medic_PainCrticialDeath04.mp3", "vo/medic_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSevere01.mp3", "vo/medic_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSevere02.mp3", "vo/medic_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSevere03.mp3", "vo/medic_LaughHappy03.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSevere04.mp3", "vo/medic_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSharp01.mp3", "vo/medic_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSharp02.mp3", "vo/medic_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSharp03.mp3", "vo/medic_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSharp04.mp3", "vo/medic_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSharp05.mp3", "vo/medic_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSharp06.mp3", "vo/medic_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSharp07.mp3", "vo/medic_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/medic_PainSharp08.mp3", "vo/medic_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/medic_AutoOnFire01.mp3", "vo/medic_PositiveVocalization01.mp3" );
+ pKVFlag->SetString( "vo/medic_AutoOnFire02.mp3", "vo/medic_PositiveVocalization02.mp3" );
+ pKVFlag->SetString( "vo/medic_AutoOnFire03.mp3", "vo/medic_PositiveVocalization03.mp3" );
+ pKVFlag->SetString( "vo/medic_AutoOnFire04.mp3", "vo/medic_PositiveVocalization04.mp3" );
+ pKVFlag->SetString( "vo/medic_AutoOnFire05.mp3", "vo/medic_PositiveVocalization05.mp3" );
+
+ pKVFlag->SetString( "vo/pyro_PainCrticialDeath01.mp3", "vo/pyro_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/pyro_PainCrticialDeath02.mp3", "vo/pyro_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/pyro_PainCrticialDeath03.mp3", "vo/pyro_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/pyro_PainSevere01.mp3", "vo/pyro_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/pyro_PainSevere02.mp3", "vo/pyro_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/pyro_PainSevere03.mp3", "vo/pyro_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/pyro_PainSevere04.mp3", "vo/pyro_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/pyro_PainSevere05.mp3", "vo/pyro_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/pyro_PainSevere06.mp3", "vo/pyro_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/pyro_AutoOnFire01.mp3", "vo/pyro_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/pyro_AutoOnFire02.mp3", "vo/pyro_LaughHappy01.mp3" );
+
+ pKVFlag->SetString( "vo/scout_PainCrticialDeath01.mp3", "vo/scout_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/scout_PainCrticialDeath02.mp3", "vo/scout_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/scout_PainCrticialDeath03.mp3", "vo/scout_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSevere01.mp3", "vo/scout_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSevere02.mp3", "vo/scout_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSevere03.mp3", "vo/scout_LaughHappy03.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSevere04.mp3", "vo/scout_LaughHappy04.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSevere05.mp3", "vo/scout_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSevere06.mp3", "vo/scout_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSharp01.mp3", "vo/scout_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSharp02.mp3", "vo/scout_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSharp03.mp3", "vo/scout_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSharp04.mp3", "vo/scout_LaughShort04.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSharp05.mp3", "vo/scout_LaughShort05.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSharp06.mp3", "vo/scout_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSharp07.mp3", "vo/scout_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/scout_PainSharp08.mp3", "vo/scout_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/scout_AutoOnFire01.mp3", "vo/scout_PositiveVocalization02.mp3" );
+ pKVFlag->SetString( "vo/scout_AutoOnFire02.mp3", "vo/scout_PositiveVocalization03.mp3" );
+
+ pKVFlag->SetString( "vo/sniper_PainCrticialDeath01.mp3", "vo/sniper_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/sniper_PainCrticialDeath02.mp3", "vo/sniper_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/sniper_PainCrticialDeath03.mp3", "vo/sniper_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/sniper_PainCrticialDeath04.mp3", "vo/sniper_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/sniper_PainSevere01.mp3", "vo/sniper_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/sniper_PainSevere02.mp3", "vo/sniper_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/sniper_PainSevere03.mp3", "vo/sniper_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/sniper_PainSevere04.mp3", "vo/sniper_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/sniper_PainSharp01.mp3", "vo/sniper_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/sniper_PainSharp02.mp3", "vo/sniper_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/sniper_PainSharp03.mp3", "vo/sniper_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/sniper_PainSharp04.mp3", "vo/sniper_LaughShort04.mp3" );
+ pKVFlag->SetString( "vo/sniper_AutoOnFire01.mp3", "vo/sniper_PositiveVocalization02.mp3" );
+ pKVFlag->SetString( "vo/sniper_AutoOnFire02.mp3", "vo/sniper_PositiveVocalization03.mp3" );
+ pKVFlag->SetString( "vo/sniper_AutoOnFire03.mp3", "vo/sniper_PositiveVocalization05.mp3" );
+
+ pKVFlag->SetString( "vo/soldier_PainCrticialDeath01.mp3", "vo/soldier_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainCrticialDeath02.mp3", "vo/soldier_LaughLong02.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainCrticialDeath03.mp3", "vo/soldier_LaughLong03.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainCrticialDeath04.mp3", "vo/soldier_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSevere01.mp3", "vo/soldier_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSevere02.mp3", "vo/soldier_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSevere03.mp3", "vo/soldier_LaughHappy03.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSevere04.mp3", "vo/soldier_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSevere05.mp3", "vo/soldier_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSevere06.mp3", "vo/soldier_LaughHappy03.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSharp01.mp3", "vo/soldier_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSharp02.mp3", "vo/soldier_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSharp03.mp3", "vo/soldier_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSharp04.mp3", "vo/soldier_LaughShort04.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSharp05.mp3", "vo/soldier_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSharp06.mp3", "vo/soldier_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSharp07.mp3", "vo/soldier_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/soldier_PainSharp08.mp3", "vo/soldier_LaughShort04.mp3" );
+ pKVFlag->SetString( "vo/soldier_AutoOnFire01.mp3", "vo/soldier_PositiveVocalization01.mp3" );
+ pKVFlag->SetString( "vo/soldier_AutoOnFire02.mp3", "vo/soldier_PositiveVocalization02.mp3" );
+ pKVFlag->SetString( "vo/soldier_AutoOnFire03.mp3", "vo/soldier_PositiveVocalization03.mp3" );
+
+ pKVFlag->SetString( "vo/spy_PainCrticialDeath01.mp3", "vo/spy_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/spy_PainCrticialDeath02.mp3", "vo/spy_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/spy_PainCrticialDeath03.mp3", "vo/spy_LaughLong01.mp3" );
+ pKVFlag->SetString( "vo/spy_PainSevere01.mp3", "vo/spy_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/spy_PainSevere02.mp3", "vo/spy_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/spy_PainSevere03.mp3", "vo/spy_LaughHappy03.mp3" );
+ pKVFlag->SetString( "vo/spy_PainSevere04.mp3", "vo/spy_LaughHappy01.mp3" );
+ pKVFlag->SetString( "vo/spy_PainSevere05.mp3", "vo/spy_LaughHappy02.mp3" );
+ pKVFlag->SetString( "vo/spy_PainSharp01.mp3", "vo/spy_LaughShort01.mp3" );
+ pKVFlag->SetString( "vo/spy_PainSharp02.mp3", "vo/spy_LaughShort02.mp3" );
+ pKVFlag->SetString( "vo/spy_PainSharp03.mp3", "vo/spy_LaughShort03.mp3" );
+ pKVFlag->SetString( "vo/spy_PainSharp04.mp3", "vo/spy_LaughShort04.mp3" );
+ pKVFlag->SetString( "vo/spy_AutoOnFire01.mp3", "vo/spy_PositiveVocalization02.mp3" );
+ pKVFlag->SetString( "vo/spy_AutoOnFire02.mp3", "vo/spy_PositiveVocalization04.mp3" );
+ pKVFlag->SetString( "vo/spy_AutoOnFire03.mp3", "vo/spy_PositiveVocalization05.mp3" );
+
+ pKVBlock->AddSubKey( pKVFlag );
+
+ // Spooky Vision
+ //pKVFlag = new KeyValues( "2" ); // TF_VISION_FILTER_HALLOWEEN
+ //pKVBlock->AddSubKey( pKVFlag );
+
+ // **************************************************************************************************
+ // WEAPONS
+ pKVBlock = new KeyValues( "weapons" );
+ m_pkvVisionFilterTranslations->AddSubKey( pKVBlock );
+
+ // No special vision
+ pKVFlag = new KeyValues( "0" );
+ pKVFlag->SetString( "models/weapons/c_models/c_rainblower/c_rainblower.mdl", "models/weapons/c_models/c_flamethrower/c_flamethrower.mdl" );
+ pKVFlag->SetString( "models/weapons/c_models/c_lollichop/c_lollichop.mdl", "models/weapons/w_models/w_fireaxe.mdl" );
+ pKVBlock->AddSubKey( pKVFlag );
+
+ // Pyrovision
+ pKVFlag = new KeyValues( "1" );
+ pKVFlag->SetString( "models/weapons/c_models/c_rainblower/c_rainblower.mdl", "models/weapons/c_models/c_rainblower/c_rainblower.mdl" ); //explicit set for pyrovision
+ pKVFlag->SetString( "models/weapons/c_models/c_lollichop/c_lollichop.mdl", "models/weapons/c_models/c_lollichop/c_lollichop.mdl" );
+
+ pKVFlag->SetString( "models/player/items/demo/demo_bombs.mdl", "models/player/items/all_class/mtp_bottle_demo.mdl" );
+ pKVFlag->SetString( "models/player/items/pyro/xms_pyro_bells.mdl", "models/player/items/all_class/mtp_bottle_pyro.mdl" );
+ pKVFlag->SetString( "models/player/items/soldier/ds_can_grenades.mdl", "models/player/items/all_class/mtp_bottle_soldier.mdl" );
+ pKVBlock->AddSubKey( pKVFlag );
+
+ // Spooky Vision
+ //pKVFlag = new KeyValues( "2" );
+ //pKVBlock->AddSubKey( pKVFlag );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::UseSillyGibs( void )
+{
+ // Use silly gibs if the local player has an item that allows them to see it (Pyro Goggles)
+ if ( IsLocalPlayerUsingVisionFilterFlags( TF_VISION_FILTER_PYRO ) )
+ return true;
+ if ( UTIL_IsLowViolence() )
+ return true;
+
+ return m_bSillyGibs;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::AllowWeatherParticles( void )
+{
+ return ( tf_particles_disable_weather.GetBool() == false );
+}
+
+bool CTFGameRules::AllowMapVisionFilterShaders( void )
+{
+ if ( !m_pkvVisionFilterShadersMapWhitelist )
+ return false;
+
+ const char *pMapName = engine->GetLevelName();
+ while ( pMapName && pMapName[ 0 ] != '\\' && pMapName[ 0 ] != '/' )
+ {
+ pMapName++;
+ }
+
+ if ( !pMapName )
+ return false;
+
+ pMapName++;
+
+ return m_pkvVisionFilterShadersMapWhitelist->GetInt( pMapName, 0 ) != 0;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+const char* CTFGameRules::TranslateEffectForVisionFilter( const char *pchEffectType, const char *pchEffectName )
+{
+ if ( !pchEffectType || !pchEffectName )
+ {
+ return pchEffectName;
+ }
+
+ CUtlVector<const char *> vecNames;
+ vecNames.AddToTail( pchEffectName );
+
+ // Swap the effect if the local player has an item that allows them to see it (Pyro Goggles)
+ bool bWeaponsOnly = FStrEq( pchEffectType, "weapons" );
+ int nVisionOptInFlags = GetLocalPlayerVisionFilterFlags( bWeaponsOnly );
+
+ KeyValues *pkvParticles = m_pkvVisionFilterTranslations->FindKey( pchEffectType );
+ if ( pkvParticles )
+ {
+ for ( KeyValues *pkvFlag = pkvParticles->GetFirstTrueSubKey(); pkvFlag != NULL; pkvFlag = pkvFlag->GetNextTrueSubKey() )
+ {
+ int nFlag = atoi( pkvFlag->GetName() );
+
+ // Grab any potential translations for this item
+ // Always grab default (No vision) names if they exist
+ if ( ( nVisionOptInFlags & nFlag ) != 0 || nFlag == 0 )
+ {
+ // We either have this vision flag or we have no flags and this is the no flag block
+ const char *pchTranslatedString = pkvFlag->GetString( pchEffectName, NULL );
+
+ if ( pchTranslatedString )
+ {
+ vecNames.AddToHead( pchTranslatedString );
+ }
+ }
+ }
+ }
+
+ // Return the top item in the list. Currently Halloween replacements would out prioritize Pyrovision if there is any overlap
+ return vecNames.Head();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::ModifySentChat( char *pBuf, int iBufSize )
+{
+ if ( IsInMedievalMode() && tf_medieval_autorp.GetBool() && english.GetBool() )
+ {
+ AutoRP()->ApplyRPTo( pBuf, iBufSize );
+ }
+
+ // replace all " with ' to prevent exploits related to chat text
+ // example: ";exit
+ for ( char *ch = pBuf; *ch != 0; ch++ )
+ {
+ if ( *ch == '"' )
+ {
+ *ch = '\'';
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::AllowMapParticleEffect( const char *pszParticleEffect )
+{
+ static const char *s_WeatherEffects[] =
+ {
+ "tf_gamerules",
+ "env_rain_001",
+ "env_rain_002_256",
+ "env_rain_ripples",
+ "env_snow_light_001",
+ "env_rain_gutterdrip",
+ "env_rain_guttersplash",
+ "", // END Marker
+ };
+
+ if ( !AllowWeatherParticles() )
+ {
+ if ( FindInList( s_WeatherEffects, pszParticleEffect ) )
+ return false;
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::GetTeamGlowColor( int nTeam, float &r, float &g, float &b )
+{
+ if ( nTeam == TF_TEAM_RED )
+ {
+ r = 0.74f;
+ g = 0.23f;
+ b = 0.23f;
+ }
+ else if ( nTeam == TF_TEAM_BLUE )
+ {
+ r = 0.49f;
+ g = 0.66f;
+ b = 0.77f;
+ }
+ else
+ {
+ r = 0.76f;
+ g = 0.76f;
+ b = 0.76f;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldConfirmOnDisconnect()
+{
+ // Add any game mode which uses matchmaking here. Note that the disconnect dialog checks if it should be showing abandons and such.
+ return ( IsMannVsMachineMode() && GTFGCClientSystem()->GetSearchPlayForBraggingRights() ) ||
+ ( IsCompetitiveMode() && GTFGCClientSystem()->GetLobby() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldShowPreRoundDoors() const
+{
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc )
+ {
+ return pMatchDesc->m_params.m_bShowPreRoundDoors;
+ }
+
+ return false;
+}
+
+#endif
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CTFGameRules::GetClassLimit( int iClass )
+{
+ if ( IsInTournamentMode() || IsPasstimeMode() )
+ {
+ switch ( iClass )
+ {
+ case TF_CLASS_SCOUT: return tf_tournament_classlimit_scout.GetInt(); break;
+ case TF_CLASS_SNIPER: return tf_tournament_classlimit_sniper.GetInt(); break;
+ case TF_CLASS_SOLDIER: return tf_tournament_classlimit_soldier.GetInt(); break;
+ case TF_CLASS_DEMOMAN: return tf_tournament_classlimit_demoman.GetInt(); break;
+ case TF_CLASS_MEDIC: return tf_tournament_classlimit_medic.GetInt(); break;
+ case TF_CLASS_HEAVYWEAPONS: return tf_tournament_classlimit_heavy.GetInt(); break;
+ case TF_CLASS_PYRO: return tf_tournament_classlimit_pyro.GetInt(); break;
+ case TF_CLASS_SPY: return tf_tournament_classlimit_spy.GetInt(); break;
+ case TF_CLASS_ENGINEER: return tf_tournament_classlimit_engineer.GetInt(); break;
+ default:
+ break;
+ }
+ }
+ else if ( IsInHighlanderMode() )
+ {
+ return 1;
+ }
+ else if ( tf_classlimit.GetInt() )
+ {
+ return tf_classlimit.GetInt();
+ }
+
+ return NO_CLASS_LIMIT;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CanPlayerChooseClass( CBasePlayer *pPlayer, int iClass )
+{
+ int iClassLimit = GetClassLimit( iClass );
+
+#ifdef TF_RAID_MODE
+ if ( IsRaidMode() && !pPlayer->IsBot() )
+ {
+ // bots are exempt from class limits, to allow for additional support bot "friends"
+ if ( pPlayer->GetTeamNumber() == TF_TEAM_BLUE )
+ {
+ if ( tf_raid_allow_all_classes.GetBool() == false )
+ {
+ if ( iClass == TF_CLASS_SCOUT )
+ return false;
+
+ if ( iClass == TF_CLASS_SPY )
+ return false;
+ }
+
+ if ( tf_raid_enforce_unique_classes.GetBool() )
+ {
+ // only one of each class on the raiding team
+ iClassLimit = 1;
+ }
+ }
+ }
+ else if ( IsBossBattleMode() )
+ {
+ return true;
+ }
+ else
+#endif // TF_RAID_MODE
+
+ if ( iClassLimit == NO_CLASS_LIMIT )
+ return true;
+
+ if ( pPlayer->GetTeamNumber() != TF_TEAM_BLUE && pPlayer->GetTeamNumber() != TF_TEAM_RED )
+ return true;
+#ifdef GAME_DLL
+ CTFTeam *pTeam = assert_cast<CTFTeam*>(pPlayer->GetTeam());
+#else
+ C_TFTeam *pTeam = assert_cast<C_TFTeam*>(pPlayer->GetTeam());
+#endif
+ if ( !pTeam )
+ return true;
+
+ int iTeamClassCount = 0;
+ for ( int iPlayer = 0; iPlayer < pTeam->GetNumPlayers(); iPlayer++ )
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( pTeam->GetPlayer( iPlayer ) );
+ if ( pTFPlayer && pTFPlayer != pPlayer && pTFPlayer->GetPlayerClass()->GetClassIndex() == iClass )
+ {
+ iTeamClassCount++;
+ }
+ }
+
+ return ( iTeamClassCount < iClassLimit );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldBalanceTeams( void )
+{
+ // never autobalance the teams for managed matches using the old system
+ if ( GetMatchGroupDescription( GetCurrentMatchGroup() ) )
+ return false;
+
+ bool bDisableBalancing = false;
+#ifdef STAGING_ONLY
+ bDisableBalancing = IsBountyMode();
+#endif // STAGING_ONLY
+
+ if ( IsPVEModeActive() || bDisableBalancing )
+ return false;
+
+ // don't balance the teams while players are in hell
+ if ( ArePlayersInHell() )
+ return false;
+
+ return BaseClass::ShouldBalanceTeams();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CTFGameRules::GetBonusRoundTime( bool bGameOver /* = false*/ )
+{
+ if ( IsMannVsMachineMode() )
+ {
+ return 5;
+ }
+ else if ( IsCompetitiveMode() && bGameOver )
+ {
+ if ( IsMatchTypeCompetitive() )
+ {
+ return 5;
+ }
+ }
+
+ return BaseClass::GetBonusRoundTime( bGameOver );
+}
+
+
+#ifdef GAME_DLL
+
+bool CTFGameRules::CanBotChangeClass( CBasePlayer* pPlayer )
+{
+ // if there's a roster for this bot's team, check to see if the level designer has allowed the bot to change class
+ // used when the bot dies and wants to see if it can change class
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+ if ( pTFPlayer && pTFPlayer->GetPlayerClass() && pTFPlayer->GetPlayerClass()->GetClassIndex() != TF_CLASS_UNDEFINED )
+ {
+ switch ( pPlayer->GetTeamNumber() )
+ {
+ case TF_TEAM_RED: return m_hRedBotRoster ? m_hRedBotRoster->IsClassChangeAllowed() : true; break;
+ case TF_TEAM_BLUE: return m_hBlueBotRoster ? m_hBlueBotRoster->IsClassChangeAllowed() : true; break;
+ }
+ }
+ return true;
+}
+
+bool CTFGameRules::CanBotChooseClass( CBasePlayer *pPlayer, int iClass )
+{
+ // if there's a roster for this bot's team, then check to see if the class the bot has requested is allowed by the roster
+ bool bCanChooseClass = CanPlayerChooseClass( pPlayer, iClass );
+ if ( bCanChooseClass )
+ {
+ // now check rosters...
+ switch ( pPlayer->GetTeamNumber() )
+ {
+ case TF_TEAM_RED:
+ bCanChooseClass = m_hRedBotRoster ? m_hRedBotRoster->IsClassAllowed( iClass ) : true;
+ break;
+ case TF_TEAM_BLUE:
+ bCanChooseClass = m_hBlueBotRoster ? m_hBlueBotRoster->IsClassAllowed( iClass ) : true;
+ break;
+ default:
+ // no roster - spectator team
+ bCanChooseClass = true;
+ break;
+ }
+ }
+ return bCanChooseClass;
+}
+
+//-----------------------------------------------------------------------------
+// Populate vector with set of control points the player needs to capture
+void CTFGameRules::CollectCapturePoints( CBasePlayer *player, CUtlVector< CTeamControlPoint * > *captureVector ) const
+{
+ if ( !captureVector )
+ return;
+
+ captureVector->RemoveAll();
+
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( pMaster )
+ {
+ // special case hack for KotH mode to use control points that are locked at the start of the round
+ if ( IsInKothMode() && pMaster->GetNumPoints() == 1 )
+ {
+ captureVector->AddToTail( pMaster->GetControlPoint( 0 ) );
+ return;
+ }
+
+ for( int i=0; i<pMaster->GetNumPoints(); ++i )
+ {
+ CTeamControlPoint *point = pMaster->GetControlPoint( i );
+ if ( point && pMaster->IsInRound( point ) )
+ {
+ if ( ObjectiveResource()->GetOwningTeam( point->GetPointIndex() ) == player->GetTeamNumber() )
+ continue;
+
+ if ( player && player->IsBot() && point->ShouldBotsIgnore() )
+ continue;
+
+ if ( ObjectiveResource()->TeamCanCapPoint( point->GetPointIndex(), player->GetTeamNumber() ) )
+ {
+ if ( TeamplayGameRules()->TeamMayCapturePoint( player->GetTeamNumber(), point->GetPointIndex() ) )
+ {
+ // unlocked point not on our team available to capture
+ captureVector->AddToTail( point );
+ }
+ }
+ }
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Populate vector with set of control points the player needs to defend from capture
+void CTFGameRules::CollectDefendPoints( CBasePlayer *player, CUtlVector< CTeamControlPoint * > *defendVector ) const
+{
+ if ( !defendVector )
+ return;
+
+ defendVector->RemoveAll();
+
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( pMaster )
+ {
+ for( int i=0; i<pMaster->GetNumPoints(); ++i )
+ {
+ CTeamControlPoint *point = pMaster->GetControlPoint( i );
+ if ( point && pMaster->IsInRound( point ) )
+ {
+ if ( ObjectiveResource()->GetOwningTeam( point->GetPointIndex() ) != player->GetTeamNumber() )
+ continue;
+
+ if ( player && player->IsBot() && point->ShouldBotsIgnore() )
+ continue;
+
+ if ( ObjectiveResource()->TeamCanCapPoint( point->GetPointIndex(), GetEnemyTeam( player->GetTeamNumber() ) ) )
+ {
+ if ( TeamplayGameRules()->TeamMayCapturePoint( GetEnemyTeam( player->GetTeamNumber() ), point->GetPointIndex() ) )
+ {
+ // unlocked point on our team vulnerable to capture
+ defendVector->AddToTail( point );
+ }
+ }
+ }
+ }
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+CObjectSentrygun *CTFGameRules::FindSentryGunWithMostKills( int team ) const
+{
+ CObjectSentrygun *dangerousSentry = NULL;
+ int dangerousSentryKills = -1;
+
+ for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i )
+ {
+ CBaseObject *pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] );
+ if ( pObj->ObjectType() == OBJ_SENTRYGUN && pObj->GetTeamNumber() == team && pObj->GetKills() >= dangerousSentryKills )
+ {
+ dangerousSentryKills = pObj->GetKills();
+ dangerousSentry = static_cast<CObjectSentrygun *>( pObj );
+ }
+ }
+
+ return dangerousSentry;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ClientConnected( edict_t *pEntity, const char *pszName, const char *pszAddress, char *reject, int maxrejectlen )
+{
+ if ( IsMannVsMachineMode() )
+ {
+ int nCount = 0;
+ CTFPlayer *pPlayer;
+
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( !pPlayer || pPlayer->IsFakeClient() )
+ continue;
+
+ nCount++;
+ }
+
+ if ( nCount >= kMVM_MaxConnectedPlayers )
+ return false;
+ }
+
+ bool bRet = BaseClass::ClientConnected( pEntity, pszName, pszAddress, reject, maxrejectlen );
+ if ( bRet )
+ {
+ const CSteamID *steamID = engine->GetClientSteamID( pEntity );
+ if ( steamID && steamID->IsValid() )
+ {
+ // Invalid steamIDs wont be known to the GC system, but it has a SteamIDAllowedToConnect() hook that would
+ // allow it to reject the connect in the first place in a matchmaking scenario where cares.
+ GTFGCClientSystem()->ClientConnected( *steamID, pEntity );
+ }
+ }
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldMakeChristmasAmmoPack( void )
+{
+ if ( IsInTournamentMode() && !IsMatchTypeCasual() )
+ return false;
+
+ if ( IsMannVsMachineMode() )
+ return false;
+
+ if ( mp_holiday_nogifts.GetBool() == true )
+ return false;
+
+ if ( IsHolidayActive( kHoliday_Christmas ) == false )
+ return false;
+
+ return ( RandomInt( 0, 100 ) < 10 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: **** DO NOT SHIP THIS IN REL/HL2 ****
+//-----------------------------------------------------------------------------
+void CTFGameRules::UpdatePeriodicEvent( CTFPlayer *pPlayer, eEconPeriodicScoreEvents eEvent, uint32 nCount )
+{
+ CSteamID steamID;
+ if ( !pPlayer || !pPlayer->GetSteamID( &steamID ) || !steamID.IsValid() )
+ return;
+
+ GCSDK::CProtoBufMsg<CMsgUpdatePeriodicEvent> msg( k_EMsgGC_UpdatePeriodicEvent );
+ msg.Body().set_account_id( steamID.GetAccountID() );
+ msg.Body().set_event_type( eEvent );
+ msg.Body().set_amount( nCount );
+ GCClientSystem()->BSendMessage( msg );
+}
+
+#endif // GAME_DLL
+
+#ifndef CLIENT_DLL
+
+void CTFGameRules::Status( void (*print) (const char *fmt, ...) )
+{
+ // print( "Total Time: %d seconds\n", CTF_GameStats.m_currentMap.m_Header.m_iTotalTime );
+ // priprint( "Blue Team Wins: %d\n", CTF_GameStats.m_currentMap.m_Header.m_iBlueWins );
+ // priprint( "Red Team Wins: %d\n", CTF_GameStats.m_currentMap.m_Header.m_iRedWins );
+ // priprint( "Stalemates: %d\n", CTF_GameStats.m_currentMap.m_Header.m_iStalemates );
+
+ print( " Spawns Points Kills Deaths Assists\n" );
+ for ( int iClass = TF_FIRST_NORMAL_CLASS; iClass < TF_LAST_NORMAL_CLASS; iClass++ )
+ {
+ TF_Gamestats_ClassStats_t &Stats = CTF_GameStats.m_currentMap.m_aClassStats[ iClass ];
+
+ print( "%-8s %6d %6d %5d %6d %7d\n",
+ g_aPlayerClassNames_NonLocalized[ iClass ],
+ Stats.iSpawns, Stats.iScore, Stats.iKills, Stats.iDeaths, Stats.iAssists );
+ }
+ print( "\n" );
+}
+
+#endif // !CLIENT_DLL
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldCollide( int collisionGroup0, int collisionGroup1 )
+{
+ if ( collisionGroup0 > collisionGroup1 )
+ {
+ // swap so that lowest is always first
+ ::V_swap( collisionGroup0, collisionGroup1 );
+ }
+
+ //Don't stand on COLLISION_GROUP_WEAPONs
+ if( collisionGroup0 == COLLISION_GROUP_PLAYER_MOVEMENT &&
+ collisionGroup1 == COLLISION_GROUP_WEAPON )
+ {
+ return false;
+ }
+
+ // Don't stand on projectiles
+ if( collisionGroup0 == COLLISION_GROUP_PLAYER_MOVEMENT &&
+ collisionGroup1 == COLLISION_GROUP_PROJECTILE )
+ {
+ return false;
+ }
+
+ // Rockets need to collide with players when they hit, but
+ // be ignored by player movement checks
+ if ( ( collisionGroup0 == COLLISION_GROUP_PLAYER ) &&
+ ( collisionGroup1 == TFCOLLISION_GROUP_ROCKETS || collisionGroup1 == TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_ROCKETS ) )
+ return true;
+
+ if ( ( collisionGroup0 == COLLISION_GROUP_PLAYER_MOVEMENT ) &&
+ ( collisionGroup1 == TFCOLLISION_GROUP_ROCKETS || collisionGroup1 == TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_ROCKETS ) )
+ return false;
+
+ if ( ( collisionGroup0 == COLLISION_GROUP_WEAPON ) &&
+ ( collisionGroup1 == TFCOLLISION_GROUP_ROCKETS || collisionGroup1 == TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_ROCKETS ) )
+ return false;
+
+ if ( ( collisionGroup0 == TF_COLLISIONGROUP_GRENADES ) &&
+ ( collisionGroup1 == TFCOLLISION_GROUP_ROCKETS || collisionGroup1 == TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_ROCKETS ) )
+ return false;
+
+ if ( ( collisionGroup0 == COLLISION_GROUP_PROJECTILE ) &&
+ ( collisionGroup1 == TFCOLLISION_GROUP_ROCKETS || collisionGroup1 == TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_ROCKETS ) )
+ return false;
+
+ if ( ( collisionGroup0 == TFCOLLISION_GROUP_ROCKETS ) &&
+ ( collisionGroup1 == TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_ROCKETS ) )
+ return false;
+
+ if ( ( collisionGroup0 == TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_ROCKETS ) &&
+ ( collisionGroup1 == TFCOLLISION_GROUP_ROCKET_BUT_NOT_WITH_OTHER_ROCKETS ) )
+ return false;
+
+ // Grenades don't collide with players. They handle collision while flying around manually.
+ if ( ( collisionGroup0 == COLLISION_GROUP_PLAYER ) &&
+ ( collisionGroup1 == TF_COLLISIONGROUP_GRENADES ) )
+ return false;
+
+ if ( ( collisionGroup0 == COLLISION_GROUP_PLAYER_MOVEMENT ) &&
+ ( collisionGroup1 == TF_COLLISIONGROUP_GRENADES ) )
+ return false;
+
+ // Respawn rooms only collide with players
+ if ( collisionGroup1 == TFCOLLISION_GROUP_RESPAWNROOMS )
+ return ( collisionGroup0 == COLLISION_GROUP_PLAYER ) || ( collisionGroup0 == COLLISION_GROUP_PLAYER_MOVEMENT );
+
+/* if ( collisionGroup0 == COLLISION_GROUP_PLAYER )
+ {
+ // Players don't collide with objects or other players
+ if ( collisionGroup1 == COLLISION_GROUP_PLAYER )
+ return false;
+ }
+
+ if ( collisionGroup1 == COLLISION_GROUP_PLAYER_MOVEMENT )
+ {
+ // This is only for probing, so it better not be on both sides!!!
+ Assert( collisionGroup0 != COLLISION_GROUP_PLAYER_MOVEMENT );
+
+ // No collide with players any more
+ // Nor with objects or grenades
+ switch ( collisionGroup0 )
+ {
+ default:
+ break;
+ case COLLISION_GROUP_PLAYER:
+ return false;
+ }
+ }
+*/
+ // don't want caltrops and other grenades colliding with each other
+ // caltops getting stuck on other caltrops, etc.)
+ if ( ( collisionGroup0 == TF_COLLISIONGROUP_GRENADES ) &&
+ ( collisionGroup1 == TF_COLLISIONGROUP_GRENADES ) )
+ {
+ return false;
+ }
+
+
+ if ( collisionGroup0 == COLLISION_GROUP_PLAYER_MOVEMENT &&
+ collisionGroup1 == TFCOLLISION_GROUP_COMBATOBJECT )
+ {
+ return false;
+ }
+
+ if ( collisionGroup0 == COLLISION_GROUP_PLAYER &&
+ collisionGroup1 == TFCOLLISION_GROUP_COMBATOBJECT )
+ {
+ return false;
+ }
+
+ if ( ( collisionGroup0 == COLLISION_GROUP_PLAYER || collisionGroup0 == COLLISION_GROUP_PLAYER_MOVEMENT ) &&
+ collisionGroup1 == TFCOLLISION_GROUP_TANK )
+ {
+ return false;
+ }
+
+ return BaseClass::ShouldCollide( collisionGroup0, collisionGroup1 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Return the value of this player towards capturing a point
+//-----------------------------------------------------------------------------
+int CTFGameRules::GetCaptureValueForPlayer( CBasePlayer *pPlayer )
+{
+#ifdef GAME_DLL
+#ifdef TF_CREEP_MODE
+ if ( IsCreepWaveMode() )
+ {
+ CTFBot *bot = ToTFBot( pPlayer );
+
+ if ( !bot || !bot->HasAttribute( CTFBot::IS_NPC ) )
+ {
+ // only creeps can influence points
+ return 0;
+ }
+ }
+#endif
+#endif
+
+ int nValue = BaseClass::GetCaptureValueForPlayer( pPlayer );
+
+
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+ if ( pTFPlayer->IsPlayerClass( TF_CLASS_SCOUT ) )
+ {
+ if ( mp_capstyle.GetInt() == 1 )
+ {
+ // Scouts count for 2 people in timebased capping.
+ nValue = 2;
+ }
+ else
+ {
+ // Scouts can cap all points on their own.
+ nValue = 10;
+ }
+ }
+
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pPlayer, nValue, add_player_capturevalue );
+
+ return nValue;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CTFGameRules::GetTimeLeft( void )
+{
+ float flTimeLimit = mp_timelimit.GetInt() * 60;
+
+ Assert( flTimeLimit > 0 && "Should not call this function when !IsGameUnderTimeLimit" );
+
+ float flMapChangeTime = m_flMapResetTime + flTimeLimit;
+
+ int iTime = (int)(flMapChangeTime - gpGlobals->curtime);
+ if ( iTime < 0 )
+ {
+ iTime = 0;
+ }
+
+ return ( iTime );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::FireGameEvent( IGameEvent *event )
+{
+ const char *eventName = event->GetName();
+
+#ifdef GAME_DLL
+ if ( !Q_strcmp( eventName, "teamplay_point_captured" ) )
+ {
+ if ( IsMannVsMachineMode() )
+ return;
+
+ // keep track of how many times each team caps
+ int iTeam = event->GetInt( "team" );
+ Assert( iTeam >= FIRST_GAME_TEAM && iTeam < TF_TEAM_COUNT );
+ m_iNumCaps[iTeam]++;
+
+ // award a capture to all capping players
+ const char *cappers = event->GetString( "cappers" );
+
+ Q_strncpy( m_szMostRecentCappers, cappers, ARRAYSIZE( m_szMostRecentCappers ) );
+ for ( int i =0; i < Q_strlen( cappers ); i++ )
+ {
+ int iPlayerIndex = (int) cappers[i];
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+ if ( pPlayer )
+ {
+ CTF_GameStats.Event_PlayerCapturedPoint( pPlayer );
+
+ if ( pPlayer->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && GetGameType() == TF_GAMETYPE_ESCORT )
+ {
+ pPlayer->AwardAchievement( ACHIEVEMENT_TF_HEAVY_PAYLOAD_CAP_GRIND );
+ }
+
+ // Give money and experience
+ int nAmount = CalculateCurrencyAmount_ByType( TF_CURRENCY_CAPTURED_OBJECTIVE );
+#ifdef STAGING_ONLY
+ if ( GameModeUsesExperience() )
+ {
+ pPlayer->AddExperiencePoints( nAmount );
+ }
+#endif // STAGING_ONLY
+ DistributeCurrencyAmount( nAmount, pPlayer, false );
+ }
+ }
+
+ // Halloween 2012 doesn't want ghosts to spawn when the point is captured
+ if( !IsHalloweenScenario( HALLOWEEN_SCENARIO_LAKESIDE ) )
+ {
+ // for 2011 Halloween map
+ BeginHaunting( 4, 25.f, 35.f );
+ }
+ }
+ else if ( !Q_strcmp( eventName, "teamplay_capture_blocked" ) )
+ {
+ int iPlayerIndex = event->GetInt( "blocker" );
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+ CTF_GameStats.Event_PlayerDefendedPoint( pPlayer );
+
+ pPlayer->m_Shared.CheckForAchievement( ACHIEVEMENT_TF_MEDIC_CHARGE_BLOCKER );
+ }
+ else if ( !Q_strcmp( eventName, "teamplay_round_win" ) )
+ {
+ int iWinningTeam = event->GetInt( "team" );
+ bool bFullRound = event->GetBool( "full_round" );
+ float flRoundTime = event->GetFloat( "round_time" );
+ bool bWasSuddenDeath = event->GetBool( "was_sudden_death" );
+ CTF_GameStats.Event_RoundEnd( iWinningTeam, bFullRound, flRoundTime, bWasSuddenDeath );
+ }
+ else if ( !Q_strcmp( eventName, "teamplay_setup_finished" ) )
+ {
+ if ( IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_DOOMSDAY ) )
+ {
+ m_doomsdaySetupTimer.Start( 1 );
+ }
+ }
+ else if ( !Q_strcmp( eventName, "teamplay_flag_event" ) )
+ {
+ // if this is a capture event, remember the player who made the capture
+ int iEventType = event->GetInt( "eventtype" );
+ if ( TF_FLAGEVENT_CAPTURE == iEventType )
+ {
+ int iPlayerIndex = event->GetInt( "player" );
+ m_szMostRecentCappers[0] = iPlayerIndex;
+ m_szMostRecentCappers[1] = 0;
+ }
+ }
+ else if ( !Q_strcmp( eventName, "player_escort_score" ) )
+ {
+ int iPlayer = event->GetInt( "player", 0 );
+ int iPoints = event->GetInt( "points", 0 );
+
+ if ( iPlayer > 0 && iPlayer <= MAX_PLAYERS )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex(iPlayer) );
+ if ( pPlayer )
+ {
+ CTF_GameStats.Event_PlayerScoresEscortPoints( pPlayer, iPoints );
+
+ int nAmount = CalculateCurrencyAmount_ByType( TF_CURRENCY_ESCORT_REWARD );
+#ifdef STAGING_ONLY
+ if ( GameModeUsesExperience() )
+ {
+ pPlayer->AddExperiencePoints( nAmount * iPoints );
+ }
+#endif // STAGING_ONLY
+ DistributeCurrencyAmount( ( nAmount * iPoints ), pPlayer, false );
+
+ if ( pPlayer->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && GetGameType() == TF_GAMETYPE_ESCORT )
+ {
+ for ( int i = 0 ; i < iPoints ; i++ )
+ {
+ pPlayer->AwardAchievement( ACHIEVEMENT_TF_HEAVY_PAYLOAD_CAP_GRIND );
+ }
+ }
+ }
+ }
+ }
+ else if ( !Q_strcmp( eventName, "player_disconnect" ) )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByUserId( event->GetInt("userid") ) );
+
+ // @note Tom Bui: this really sucks, but we don't know the reason other than the string...
+ const char *pReason = event->GetString( "reason" );
+ if ( !Q_strncmp( pReason, "Kicked", ARRAYSIZE( "Kicked" ) - 1 ) )
+ {
+ if ( pPlayer )
+ {
+ DuelMiniGame_NotifyPlayerDisconnect( pPlayer, true );
+ }
+ }
+ }
+ else if ( !Q_strcmp( eventName, "teamplay_round_start" ) )
+ {
+ if ( IsMannVsMachineMode() )
+ {
+ if ( g_pPopulationManager )
+ {
+ g_pPopulationManager->RestorePlayerCurrency();
+
+ // make sure all invaders are removed
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_PVE_INVADERS );
+
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ playerVector[i]->ChangeTeam( TEAM_SPECTATOR, false, true );
+ }
+ }
+ }
+ }
+ else if ( !Q_strcmp( eventName, "recalculate_truce" ) )
+ {
+ RecalculateTruce();
+ }
+#else // CLIENT_DLL
+ if ( !Q_strcmp( eventName, "overtime_nag" ) )
+ {
+ HandleOvertimeBegin();
+ }
+ else if ( !Q_strcmp( eventName, "recalculate_holidays" ) )
+ {
+ UTIL_CalculateHolidays();
+ }
+#endif
+
+ BaseClass::FireGameEvent( event );
+}
+
+
+const char *CTFGameRules::GetGameTypeName( void )
+{
+ return ::GetGameTypeName( m_nGameType.Get() );
+}
+
+
+void CTFGameRules::ClientSpawned( edict_t * pPlayer )
+{
+}
+
+void CTFGameRules::OnFileReceived( const char * fileName, unsigned int transferID )
+{
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Init ammo definitions
+//-----------------------------------------------------------------------------
+
+// shared ammo definition
+// JAY: Trying to make a more physical bullet response
+#define BULLET_MASS_GRAINS_TO_LB(grains) (0.002285*(grains)/16.0f)
+#define BULLET_MASS_GRAINS_TO_KG(grains) lbs2kg(BULLET_MASS_GRAINS_TO_LB(grains))
+
+// exaggerate all of the forces, but use real numbers to keep them consistent
+#define BULLET_IMPULSE_EXAGGERATION 1
+
+// convert a velocity in ft/sec and a mass in grains to an impulse in kg in/s
+#define BULLET_IMPULSE(grains, ftpersec) ((ftpersec)*12*BULLET_MASS_GRAINS_TO_KG(grains)*BULLET_IMPULSE_EXAGGERATION)
+
+
+CAmmoDef* GetAmmoDef()
+{
+ static CAmmoDef def;
+ static bool bInitted = false;
+
+ if ( !bInitted )
+ {
+ bInitted = true;
+
+ // Start at 1 here and skip the dummy ammo type to make CAmmoDef use the same indices
+ // as our #defines.
+ for ( int i=1; i < TF_AMMO_COUNT; i++ )
+ {
+ const char *pszAmmoName = GetAmmoName( i );
+ def.AddAmmoType( pszAmmoName, DMG_BULLET | DMG_USEDISTANCEMOD | DMG_NOCLOSEDISTANCEMOD, TRACER_LINE, 0, 0, "ammo_max", 2400, 10, 14 );
+ Assert( def.Index( pszAmmoName ) == i );
+ }
+ }
+
+ return &def;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+const char *CTFGameRules::GetTeamGoalString( int iTeam )
+{
+ if ( iTeam == TF_TEAM_RED )
+ return m_pszTeamGoalStringRed.Get();
+ if ( iTeam == TF_TEAM_BLUE )
+ return m_pszTeamGoalStringBlue.Get();
+ return NULL;
+}
+
+#ifdef GAME_DLL
+
+ Vector MaybeDropToGround(
+ CBaseEntity *pMainEnt,
+ bool bDropToGround,
+ const Vector &vPos,
+ const Vector &vMins,
+ const Vector &vMaxs )
+ {
+ if ( bDropToGround )
+ {
+ trace_t trace;
+ UTIL_TraceHull( vPos, vPos + Vector( 0, 0, -500 ), vMins, vMaxs, MASK_SOLID, pMainEnt, COLLISION_GROUP_NONE, &trace );
+ return trace.endpos;
+ }
+ else
+ {
+ return vPos;
+ }
+ }
+
+ //-----------------------------------------------------------------------------
+ // Purpose: This function can be used to find a valid placement location for an entity.
+ // Given an origin to start looking from and a minimum radius to place the entity at,
+ // it will sweep out a circle around vOrigin and try to find a valid spot (on the ground)
+ // where mins and maxs will fit.
+ // Input : *pMainEnt - Entity to place
+ // &vOrigin - Point to search around
+ // fRadius - Radius to search within
+ // nTries - Number of tries to attempt
+ // &mins - mins of the Entity
+ // &maxs - maxs of the Entity
+ // &outPos - Return point
+ // Output : Returns true and fills in outPos if it found a spot.
+ //-----------------------------------------------------------------------------
+ bool EntityPlacementTest( CBaseEntity *pMainEnt, const Vector &vOrigin, Vector &outPos, bool bDropToGround )
+ {
+ // This function moves the box out in each dimension in each step trying to find empty space like this:
+ //
+ // X
+ // X X
+ // Step 1: X Step 2: XXX Step 3: XXXXX
+ // X X
+ // X
+ //
+
+ Vector mins, maxs;
+ pMainEnt->CollisionProp()->WorldSpaceAABB( &mins, &maxs );
+ mins -= pMainEnt->GetAbsOrigin();
+ maxs -= pMainEnt->GetAbsOrigin();
+
+ // Put some padding on their bbox.
+ float flPadSize = 5;
+ Vector vTestMins = mins - Vector( flPadSize, flPadSize, flPadSize );
+ Vector vTestMaxs = maxs + Vector( flPadSize, flPadSize, flPadSize );
+
+ // First test the starting origin.
+ if ( UTIL_IsSpaceEmpty( pMainEnt, vOrigin + vTestMins, vOrigin + vTestMaxs ) )
+ {
+ outPos = MaybeDropToGround( pMainEnt, bDropToGround, vOrigin, vTestMins, vTestMaxs );
+ return true;
+ }
+
+ Vector vDims = vTestMaxs - vTestMins;
+
+
+ // Keep branching out until we get too far.
+ int iCurIteration = 0;
+ int nMaxIterations = 15;
+
+ int offset = 0;
+ do
+ {
+ for ( int iDim=0; iDim < 3; iDim++ )
+ {
+ float flCurOffset = offset * vDims[iDim];
+
+ for ( int iSign=0; iSign < 2; iSign++ )
+ {
+ Vector vBase = vOrigin;
+ vBase[iDim] += (iSign*2-1) * flCurOffset;
+
+ if ( UTIL_IsSpaceEmpty( pMainEnt, vBase + vTestMins, vBase + vTestMaxs ) )
+ {
+ // Ensure that there is a clear line of sight from the spawnpoint entity to the actual spawn point.
+ // (Useful for keeping things from spawning behind walls near a spawn point)
+ trace_t tr;
+ UTIL_TraceLine( vOrigin, vBase, MASK_SOLID, pMainEnt, COLLISION_GROUP_NONE, &tr );
+
+ if ( tr.fraction != 1.0 )
+ {
+ continue;
+ }
+
+ outPos = MaybeDropToGround( pMainEnt, bDropToGround, vBase, vTestMins, vTestMaxs );
+ return true;
+ }
+ }
+ }
+
+ ++offset;
+ } while ( iCurIteration++ < nMaxIterations );
+
+ // Warning( "EntityPlacementTest for ent %d:%s failed!\n", pMainEnt->entindex(), pMainEnt->GetClassname() );
+ return false;
+ }
+
+#else // GAME_DLL
+
+CTFGameRules::~CTFGameRules()
+{
+ if ( m_pkvVisionFilterTranslations )
+ {
+ m_pkvVisionFilterTranslations->deleteThis();
+ m_pkvVisionFilterTranslations = NULL;
+ }
+
+ if ( m_pkvVisionFilterShadersMapWhitelist )
+ {
+ m_pkvVisionFilterShadersMapWhitelist->deleteThis();
+ m_pkvVisionFilterShadersMapWhitelist = NULL;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::OnDataChanged( DataUpdateType_t updateType )
+{
+ BaseClass::OnDataChanged( updateType );
+
+ m_bRecievedBaseline |= updateType == DATA_UPDATE_CREATED;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::HandleOvertimeBegin()
+{
+ C_TFPlayer *pTFPlayer = C_TFPlayer::GetLocalTFPlayer();
+ if ( pTFPlayer )
+ {
+ pTFPlayer->EmitSound( "Game.Overtime" );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldShowTeamGoal( void )
+{
+
+//=============================================================================
+// HPE_BEGIN
+// [msmith] We always show the team goal when in training.
+//=============================================================================
+ bool showDuringSetup = InSetup();
+
+ if ( IsInTraining() || IsInItemTestingMode() )
+ {
+ showDuringSetup = false;
+ }
+
+ if ( State_Get() == GR_STATE_PREROUND || State_Get() == GR_STATE_RND_RUNNING || showDuringSetup )
+ return true;
+
+//=============================================================================
+// HPE_END
+//=============================================================================
+ return false;
+}
+
+#endif
+
+#ifdef GAME_DLL
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::ShutdownCustomResponseRulesDicts()
+{
+ DestroyCustomResponseSystems();
+
+ if ( m_ResponseRules.Count() != 0 )
+ {
+ int nRuleCount = m_ResponseRules.Count();
+ for ( int iRule = 0; iRule < nRuleCount; ++iRule )
+ {
+ m_ResponseRules[iRule].m_ResponseSystems.Purge();
+ }
+ m_ResponseRules.Purge();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::InitCustomResponseRulesDicts()
+{
+ MEM_ALLOC_CREDIT();
+
+ // Clear if necessary.
+ ShutdownCustomResponseRulesDicts();
+
+ // Initialize the response rules for TF.
+ m_ResponseRules.AddMultipleToTail( TF_CLASS_COUNT_ALL );
+
+ char szName[512];
+ for ( int iClass = TF_FIRST_NORMAL_CLASS; iClass < TF_CLASS_COUNT_ALL; ++iClass )
+ {
+ m_ResponseRules[iClass].m_ResponseSystems.AddMultipleToTail( MP_TF_CONCEPT_COUNT );
+
+ for ( int iConcept = 0; iConcept < MP_TF_CONCEPT_COUNT; ++iConcept )
+ {
+ AI_CriteriaSet criteriaSet;
+ criteriaSet.AppendCriteria( "playerclass", g_aPlayerClassNames_NonLocalized[iClass] );
+ criteriaSet.AppendCriteria( "Concept", g_pszMPConcepts[iConcept] );
+
+ // 1 point for player class and 1 point for concept.
+ float flCriteriaScore = 2.0f;
+
+ // Name.
+ V_sprintf_safe( szName, "%s_%s\n", g_aPlayerClassNames_NonLocalized[iClass], g_pszMPConcepts[iConcept] );
+ m_ResponseRules[iClass].m_ResponseSystems[iConcept] = BuildCustomResponseSystemGivenCriteria( "scripts/talker/response_rules.txt", szName, criteriaSet, flCriteriaScore );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+#ifdef _DEBUG
+CON_COMMAND( hud_notify, "Show a hud notification." )
+{
+ if ( args.ArgC() < 2 )
+ {
+ Warning( "Requires one argument, HudNotification_t, between 0 and %i\n", NUM_STOCK_NOTIFICATIONS );
+ return;
+ }
+
+ if ( !TFGameRules() )
+ {
+ Warning( "Can't do that right now\n" );
+ return;
+ }
+
+ CRecipientFilter filter;
+ filter.AddAllPlayers();
+ TFGameRules()->SendHudNotification( filter, (HudNotification_t) V_atoi(args.Arg(1)) );
+}
+#endif
+
+void CTFGameRules::SendHudNotification( IRecipientFilter &filter, HudNotification_t iType, bool bForceShow /*= false*/ )
+{
+ if ( !bForceShow && IsInWaitingForPlayers() )
+ return;
+
+ UserMessageBegin( filter, "HudNotify" );
+ WRITE_BYTE( iType );
+ WRITE_BOOL( bForceShow ); // Display in cl_hud_minmode
+ MessageEnd();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SendHudNotification( IRecipientFilter &filter, const char *pszText, const char *pszIcon, int iTeam /*= TEAM_UNASSIGNED*/ )
+{
+ if ( IsInWaitingForPlayers() )
+ return;
+
+ UserMessageBegin( filter, "HudNotifyCustom" );
+ WRITE_STRING( pszText );
+ WRITE_STRING( pszIcon );
+ WRITE_BYTE( iTeam );
+ MessageEnd();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Is the player past the required delays for spawning
+//-----------------------------------------------------------------------------
+bool CTFGameRules::HasPassedMinRespawnTime( CBasePlayer *pPlayer )
+{
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+
+ if ( pTFPlayer && pTFPlayer->GetPlayerClass()->GetClassIndex() == TF_CLASS_UNDEFINED )
+ return true;
+
+ float flMinSpawnTime = GetMinTimeWhenPlayerMaySpawn( pPlayer );
+
+ return ( gpGlobals->curtime > flMinSpawnTime );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldRespawnQuickly( CBasePlayer *pPlayer )
+{
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+ if ( IsPVEModeActive() && pTFPlayer && pTFPlayer->GetTeamNumber() == TF_TEAM_PVE_DEFENDERS && pTFPlayer->GetPlayerClass()->GetClassIndex() == TF_CLASS_SCOUT )
+ {
+ return true;
+ }
+
+#if defined( _DEBUG ) || defined( STAGING_ONLY )
+ if ( mp_developer.GetBool() )
+ return true;
+#endif // _DEBUG || STAGING_ONLY
+
+ if ( IsCompetitiveMode() && State_Get() == GR_STATE_BETWEEN_RNDS )
+ return true;
+
+ return BaseClass::ShouldRespawnQuickly( pPlayer );
+}
+
+typedef bool (*BIgnoreConvarChangeFunc)(void);
+
+struct convar_tags_t
+{
+ const char *pszConVar;
+ const char *pszTag;
+ BIgnoreConvarChangeFunc ignoreConvarFunc;
+};
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+static bool BIgnoreConvarChangeInPVEMode(void)
+{
+ return TFGameRules() && TFGameRules()->IsPVEModeActive();
+}
+
+// The list of convars that automatically turn on tags when they're changed.
+// Convars in this list need to have the FCVAR_NOTIFY flag set on them, so the
+// tags are recalculated and uploaded to the master server when the convar is changed.
+convar_tags_t convars_to_check_for_tags[] =
+{
+ { "mp_friendlyfire", "friendlyfire", NULL },
+ { "tf_birthday", "birthday", NULL },
+ { "mp_respawnwavetime", "respawntimes", NULL },
+ { "mp_fadetoblack", "fadetoblack", NULL },
+ { "tf_weapon_criticals", "nocrits", NULL },
+ { "mp_disable_respawn_times", "norespawntime", NULL },
+ { "tf_gamemode_arena", "arena", NULL },
+ { "tf_gamemode_cp", "cp", NULL },
+ { "tf_gamemode_ctf", "ctf", NULL },
+ { "tf_gamemode_sd", "sd", NULL },
+ { "tf_gamemode_mvm", "mvm", NULL },
+ { "tf_gamemode_payload", "payload", NULL },
+ { "tf_gamemode_rd", "rd", NULL },
+ { "tf_gamemode_pd", "pd", NULL },
+ { "tf_gamemode_tc", "tc", NULL },
+ { "tf_beta_content", "beta", NULL },
+ { "tf_damage_disablespread", "dmgspread", NULL },
+ { "mp_highlander", "highlander", NULL },
+ { "tf_bot_count", "bots", &BIgnoreConvarChangeInPVEMode },
+ { "tf_pve_mode", "pve" },
+ { "sv_registration_successful", "_registered", NULL },
+ { "tf_server_identity_disable_quickplay", "noquickplay", NULL },
+ { "tf_mm_strict", "hidden", NULL },
+ { "tf_medieval", "medieval", NULL },
+ { "mp_holiday_nogifts", "nogifts" },
+ { "tf_powerup_mode", "powerup", NULL },
+ { "tf_gamemode_passtime", "passtime", NULL },
+ { "tf_gamemode_misc", "misc", NULL }, // catch-all for matchmaking to identify sd, tc, and pd servers via sv_tags
+};
+
+//-----------------------------------------------------------------------------
+// Purpose: Engine asks for the list of convars that should tag the server
+//-----------------------------------------------------------------------------
+void CTFGameRules::GetTaggedConVarList( KeyValues *pCvarTagList )
+{
+ BaseClass::GetTaggedConVarList( pCvarTagList );
+
+ for ( int i = 0; i < ARRAYSIZE(convars_to_check_for_tags); i++ )
+ {
+ if ( convars_to_check_for_tags[i].ignoreConvarFunc && convars_to_check_for_tags[i].ignoreConvarFunc() )
+ continue;
+
+ KeyValues *pKV = new KeyValues( "tag" );
+ pKV->SetString( "convar", convars_to_check_for_tags[i].pszConVar );
+ pKV->SetString( "tag", convars_to_check_for_tags[i].pszTag );
+
+ pCvarTagList->AddSubKey( pKV );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::PlaySpecialCapSounds( int iCappingTeam, CTeamControlPoint *pPoint )
+{
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_LAKESIDE ) )
+ {
+ return;
+ }
+
+ if ( GetGameType() == TF_GAMETYPE_CP )
+ {
+ bool bPlayControlPointCappedSound = IsInKothMode();
+ if ( !bPlayControlPointCappedSound )
+ {
+ if ( pPoint && ShouldScorePerRound() )
+ {
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( pMaster && !pMaster->WouldNewCPOwnerWinGame( pPoint, iCappingTeam ) )
+ {
+ bPlayControlPointCappedSound = true;
+ }
+ }
+ }
+
+ if ( bPlayControlPointCappedSound )
+ {
+ for ( int i = FIRST_GAME_TEAM; i < GetNumberOfTeams(); i++ )
+ {
+ if ( IsInKothMode() )
+ {
+ BroadcastSound( i, "Hud.PointCaptured" );
+ }
+
+ if ( i == iCappingTeam )
+ {
+ BroadcastSound( i, "Announcer.Success" );
+ }
+ else
+ {
+ BroadcastSound( i, "Announcer.Failure" );
+ }
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Factory to create TF-specific mission manager singleton
+//-----------------------------------------------------------------------------
+CTacticalMissionManager *CTFGameRules::TacticalMissionManagerFactory( void )
+{
+ return new CTFTacticalMissionManager;
+}
+
+#endif
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Get the video file name for the current map.
+//-----------------------------------------------------------------------------
+#ifdef CLIENT_DLL
+const char *CTFGameRules::GetVideoFileForMap( bool bWithExtension /*= true*/ )
+{
+ char mapname[MAX_MAP_NAME];
+ mapname[0] = 0;
+
+ Q_FileBase( engine->GetLevelName(), mapname, sizeof( mapname ) );
+ if ( mapname[0] == 0 )
+ {
+ return NULL;
+ }
+
+ Q_strlower( mapname );
+ return FormatVideoName( (const char *)mapname, bWithExtension );
+}
+
+//=============================================================================
+// HPE_BEGIN
+// [msmith] Used for the client to tell the server that we're watching a movie or not.
+// Also contains the name of a movie if it's an in game video.
+//=============================================================================
+//-----------------------------------------------------------------------------
+// Purpose: Format a video file name from the name passed in.
+//-----------------------------------------------------------------------------
+const char *CTFGameRules::FormatVideoName( const char *videoName, bool bWithExtension /*= true*/ )
+{
+
+ static char strFullpath[MAX_PATH]; // this buffer is returned to the caller
+
+#ifdef _X360
+ // Should we be modifying a const buffer?
+ // need to remove the .360 extension on the end of the map name
+ char *pExt = Q_stristr( videoName, ".360" );
+ if ( pExt )
+ {
+ *pExt = '\0';
+ }
+#endif
+
+ Q_strncpy( strFullpath, "media/", MAX_PATH ); // Assume we must play out of the media directory
+
+ if ( Q_strstr( videoName, "arena_" ) )
+ {
+ char strTempPath[MAX_PATH];
+ Q_strncpy( strTempPath, "media/", MAX_PATH );
+ Q_strncat( strTempPath, videoName, MAX_PATH );
+ Q_strncat( strTempPath, FILE_EXTENSION_ANY_MATCHING_VIDEO, MAX_PATH );
+
+ VideoSystem_t vSystem = VideoSystem::NONE;
+
+ // default to arena_intro video if we can't find the specified video
+ if ( !g_pVideo || g_pVideo->LocatePlayableVideoFile( strTempPath, "GAME", &vSystem, strFullpath, sizeof(strFullpath), VideoSystemFeature::PLAY_VIDEO_FILE_IN_MATERIAL ) != VideoResult::SUCCESS )
+ {
+ V_strncpy( strFullpath, "media/" "arena_intro", MAX_PATH );
+ }
+ }
+ else if ( Q_strstr( videoName, "mvm_" ) )
+ {
+ char strTempPath[MAX_PATH];
+ Q_strncpy( strTempPath, "media/", MAX_PATH );
+ Q_strncat( strTempPath, videoName, MAX_PATH );
+ Q_strncat( strTempPath, FILE_EXTENSION_ANY_MATCHING_VIDEO, MAX_PATH );
+
+ VideoSystem_t vSystem = VideoSystem::NONE;
+
+ // default to mvm_intro video if we can't find the specified video
+ if ( !g_pVideo || g_pVideo->LocatePlayableVideoFile( strTempPath, "GAME", &vSystem, strFullpath, sizeof(strFullpath), VideoSystemFeature::PLAY_VIDEO_FILE_IN_MATERIAL ) != VideoResult::SUCCESS )
+ {
+ V_strncpy( strFullpath, "media/" "mvm_intro", MAX_PATH );
+ }
+ }
+ else
+ {
+ Q_strncat( strFullpath, videoName, MAX_PATH );
+ }
+
+ if ( bWithExtension )
+ {
+ Q_strncat( strFullpath, FILE_EXTENSION_ANY_MATCHING_VIDEO, MAX_PATH ); // Don't assume any specific video type, let the video services find it
+ }
+
+ return strFullpath;
+}
+#endif
+//=============================================================================
+// HPE_END
+//=============================================================================
+
+#ifdef CLIENT_DLL
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+const char *GetMapDisplayName( const char *mapName )
+{
+ static char szDisplayName[256];
+ char szTempName[256];
+ const char *pszSrc = NULL;
+
+ szDisplayName[0] = '\0';
+
+ if ( !mapName )
+ return szDisplayName;
+
+ // check our lookup table
+ Q_strncpy( szTempName, mapName, sizeof( szTempName ) );
+ Q_strlower( szTempName );
+ pszSrc = szTempName;
+
+ for ( int i = 0; i < ARRAYSIZE( s_ValveMaps ); ++i )
+ {
+ if ( !Q_stricmp( s_ValveMaps[i].pDiskName, pszSrc ) )
+ {
+ return s_ValveMaps[i].pDisplayName;
+ }
+ }
+
+ // check the community maps that we've featured
+ for ( int i = 0; i < ARRAYSIZE( s_CommunityMaps ); ++i )
+ {
+ if ( !Q_stricmp( s_CommunityMaps[i].pDiskName, pszSrc ) )
+ {
+ return s_CommunityMaps[i].pDisplayName;
+ }
+ }
+
+ char *pszFinal = Q_strstr( pszSrc, "_final" );
+ if ( pszFinal )
+ {
+ // truncate the _final (or _final1) part of the filename if it's at the end of the name
+ char *pszNextChar = pszFinal + Q_strlen( "_final" );
+ if ( pszNextChar )
+ {
+ if ( ( *pszNextChar == '\0' ) ||
+ ( ( *pszNextChar == '1' ) && ( *(pszNextChar+1) == '\0' ) ) )
+ {
+ *pszFinal = '\0';
+ }
+ }
+ }
+
+ // Our workshop maps will be of the format workshop/cp_somemap.ugc12345
+ const char szWorkshop[] = "workshop/";
+ if ( V_strncmp( pszSrc, szWorkshop, sizeof( szWorkshop ) - 1 ) == 0 )
+ {
+ pszSrc += sizeof( szWorkshop ) - 1;
+ char *pszUGC = V_strstr( pszSrc, ".ugc" );
+ int nUGCLen = pszUGC ? strlen( pszUGC ) : 0;
+ if ( pszUGC && nUGCLen > 4 )
+ {
+ int i;
+ for ( i = 4; i < nUGCLen; i ++ )
+ {
+ if ( pszUGC[i] < '0' || pszUGC[i] > '9' )
+ {
+ break;
+ }
+ }
+
+ if ( i == nUGCLen )
+ {
+ *pszUGC = '\0';
+ }
+ }
+ }
+
+ // we haven't found a "friendly" map name, so let's just clean up what we have
+ if ( !Q_strncmp( pszSrc, "cp_", 3 ) ||
+ !Q_strncmp( pszSrc, "tc_", 3 ) ||
+ !Q_strncmp( pszSrc, "pl_", 3 ) ||
+ !Q_strncmp( pszSrc, "ad_", 3 ) ||
+ !Q_strncmp( pszSrc, "sd_", 3 ) ||
+ !Q_strncmp( pszSrc, "rd_", 3 ) ||
+ !Q_strncmp( pszSrc, "pd_", 3 ) )
+ {
+ pszSrc += 3;
+ }
+ else if ( !Q_strncmp( pszSrc, "ctf_", 4 ) ||
+ !Q_strncmp( pszSrc, "plr_", 4 ) )
+ {
+ pszSrc += 4;
+ }
+ else if ( !Q_strncmp( szTempName, "koth_", 5 ) ||
+ !Q_strncmp( szTempName, "pass_", 5 ) )
+ {
+ pszSrc += 5;
+ }
+#ifdef TF_RAID_MODE
+ else if ( !Q_strncmp( pszSrc, "raid_", 5 ) )
+ {
+ pszSrc += 5;
+ }
+#endif // TF_RAID_MODE
+ else if ( !Q_strncmp( pszSrc, "mvm_", 4 ) )
+ {
+ pszSrc += 4;
+ }
+ else if ( !Q_strncmp( pszSrc, "arena_", 6 ) )
+ {
+ pszSrc += 6;
+ }
+
+ Q_strncpy( szDisplayName, pszSrc, sizeof( szDisplayName ) );
+ Q_strupr( szDisplayName );
+
+ // replace underscores with spaces
+ for ( char *pszUnderscore = szDisplayName ; pszUnderscore != NULL && *pszUnderscore != 0 ; pszUnderscore++ )
+ {
+ // Replace it with a space
+ if ( *pszUnderscore == '_' )
+ {
+ *pszUnderscore = ' ';
+ }
+ }
+
+ return szDisplayName;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+const char *GetMapType( const char *mapName )
+{
+ int i;
+
+ if ( mapName )
+ {
+ for ( i = 0; i < ARRAYSIZE( s_ValveMaps ); ++i )
+ {
+ if ( !Q_stricmp( s_ValveMaps[i].pDiskName, mapName ) )
+ {
+ return s_ValveMaps[i].pGameType;
+ }
+ }
+
+ // check the community maps that we've featured
+ for ( i = 0; i < ARRAYSIZE( s_CommunityMaps ); ++i )
+ {
+ if ( !Q_stricmp( s_CommunityMaps[i].pDiskName, mapName ) )
+ {
+ return s_CommunityMaps[i].pGameType;
+ }
+ }
+ }
+
+ if ( !IsX360() )
+ {
+ // we haven't found a "friendly" map name, so let's just clean up what we have
+ if ( !Q_strnicmp( mapName, "cp_", 3 ) )
+ {
+ return "#Gametype_CP";
+ }
+ else if ( !Q_strnicmp( mapName, "tc_", 3 ) )
+ {
+ return "#TF_TerritoryControl";
+ }
+ else if ( !Q_strnicmp( mapName, "pl_", 3 ) )
+ {
+ return "#Gametype_Escort";
+ }
+ else if ( !Q_strnicmp( mapName, "plr_", 4 ) )
+ {
+ return "#Gametype_EscortRace";
+ }
+ else if ( !Q_strnicmp( mapName, "ad_", 3 ) )
+ {
+ return "#TF_AttackDefend";
+ }
+ else if ( !Q_strnicmp( mapName, "ctf_", 4 ) )
+ {
+ return "#Gametype_CTF";
+ }
+ else if ( !Q_strnicmp( mapName, "koth_", 5 ) )
+ {
+ return "#Gametype_Koth";
+ }
+ else if ( !Q_strnicmp( mapName, "arena_", 6 ) )
+ {
+ return "#Gametype_Arena";
+ }
+ else if ( !Q_strnicmp( mapName, "sd_", 3 ) )
+ {
+ return "#Gametype_SD";
+ }
+#ifdef TF_RAID_MODE
+ else if ( !Q_strnicmp( mapName, "raid_", 5 ) )
+ {
+ return "#Gametype_Raid";
+ }
+#endif // TF_RAID_MODE
+ else if ( !Q_strnicmp( mapName, "mvm_", 4 ) )
+ {
+ return "#Gametype_MVM";
+ }
+ else if ( !Q_strnicmp( mapName, "pass_", 5 ) )
+ {
+ return "#GameType_Passtime";
+ }
+ else if ( !Q_strnicmp( mapName, "rd_", 3 ) )
+ {
+ return "#Gametype_RobotDestruction";
+ }
+ else if ( !Q_strnicmp( mapName, "pd_", 3 ) )
+ {
+ return "#Gametype_PlayerDestruction";
+ }
+ else
+ {
+ if ( TFGameRules() )
+ {
+ return TFGameRules()->GetGameTypeName();
+ }
+ }
+ }
+
+ return "";
+}
+#endif
+
+//Arena Mode
+bool CTFGameRules::IsInArenaMode( void ) const
+{
+ return m_nGameType == TF_GAMETYPE_ARENA;
+}
+
+#ifdef GAME_DLL
+
+//==================================================================================================================
+// ARENA LOGIC
+BEGIN_DATADESC( CArenaLogic )
+ DEFINE_OUTPUT( m_OnArenaRoundStart, "OnArenaRoundStart" ),
+ DEFINE_OUTPUT( m_OnCapEnabled, "OnCapEnabled" ),
+ DEFINE_KEYFIELD( m_flTimeToEnableCapPoint, FIELD_FLOAT, "CapEnableDelay" ),
+ DEFINE_FUNCTION( ArenaLogicThink ),
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( tf_logic_arena, CArenaLogic );
+
+
+void CArenaLogic::Spawn( void )
+{
+ BaseClass::Spawn();
+
+ SetThink( &CArenaLogic::ArenaLogicThink );
+ SetNextThink( gpGlobals->curtime );
+}
+
+void CArenaLogic::ArenaLogicThink( void )
+{
+ SetNextThink( gpGlobals->curtime + 0.1f );
+
+ if ( TFGameRules()->State_Get() != GR_STATE_STALEMATE )
+ return;
+
+ if ( TFGameRules() && TFGameRules()->GetCapturePointTime() <= gpGlobals->curtime )
+ {
+ if ( m_bFiredOutput == false )
+ {
+ m_bFiredOutput = true;
+ m_OnCapEnabled.FireOutput( this, this );
+ }
+ }
+ else if ( TFGameRules() && TFGameRules()->GetCapturePointTime() > gpGlobals->curtime )
+ {
+ m_bFiredOutput = false;
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: COMPETITIVE LOGIC (why are we shouting?)
+//-----------------------------------------------------------------------------
+BEGIN_DATADESC( CCompetitiveLogic )
+DEFINE_OUTPUT( m_OnSpawnRoomDoorsShouldLock, "OnSpawnRoomDoorsShouldLock" ),
+DEFINE_OUTPUT( m_OnSpawnRoomDoorsShouldUnlock, "OnSpawnRoomDoorsShouldUnlock" ),
+END_DATADESC()
+LINK_ENTITY_TO_CLASS( tf_logic_competitive, CCompetitiveLogic );
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCompetitiveLogic::OnSpawnRoomDoorsShouldLock( void )
+{
+ if ( !TFGameRules() || !TFGameRules()->IsCompetitiveMode() )
+ return;
+
+ m_OnSpawnRoomDoorsShouldLock.FireOutput( this, this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCompetitiveLogic::OnSpawnRoomDoorsShouldUnlock( void )
+{
+ if ( !TFGameRules() || !TFGameRules()->IsCompetitiveMode() )
+ return;
+
+ m_OnSpawnRoomDoorsShouldUnlock.FireOutput( this, this );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+BEGIN_DATADESC( CLogicMannPower )
+END_DATADESC()
+LINK_ENTITY_TO_CLASS( tf_logic_mannpower, CLogicMannPower );
+
+//=============================================================================
+// Training Mode
+CON_COMMAND( training_continue, "Tells training that it should continue." )
+{
+ if ( TFGameRules() == NULL || TFGameRules()->IsInTraining() == false || TFGameRules()->GetTrainingModeLogic() == NULL )
+ {
+ return;
+ }
+ TFGameRules()->GetTrainingModeLogic()->OnPlayerWantsToContinue();
+}
+
+#define SF_TF_DYNAMICPROP_GRENADE_COLLISION 512
+class CTFTrainingDynamicProp : public CDynamicProp
+{
+ DECLARE_CLASS( CTFTrainingDynamicProp, CDynamicProp );
+public:
+};
+LINK_ENTITY_TO_CLASS( training_prop_dynamic, CTFTrainingDynamicProp );
+
+bool PropDynamic_CollidesWithGrenades( CBaseEntity *pBaseEntity )
+{
+ CTFTrainingDynamicProp *pTrainingDynamicProp = dynamic_cast< CTFTrainingDynamicProp* >( pBaseEntity );
+ return ( pTrainingDynamicProp && pTrainingDynamicProp->HasSpawnFlags( SF_TF_DYNAMICPROP_GRENADE_COLLISION) );
+}
+
+BEGIN_DATADESC( CTrainingModeLogic )
+ DEFINE_KEYFIELD( m_nextMapName, FIELD_STRING, "nextMap" ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "ShowTrainingMsg", InputShowTrainingMsg ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "ShowTrainingObjective", InputShowTrainingObjective ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "ForcePlayerSpawnAsClassOutput", InputForcePlayerSpawnAsClassOutput ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "KickBots", InputKickAllBots ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "ShowTrainingHUD", InputShowTrainingHUD ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "HideTrainingHUD", InputHideTrainingHUD ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "EndTraining", InputEndTraining ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "PlaySoundOnPlayer", InputPlaySoundOnPlayer ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "WaitForTimerOrKeypress", InputWaitForTimerOrKeypress ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "SetNextMap", InputSetNextMap ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "ForcePlayerSwapToWeapon", InputForcePlayerSwapToWeapon ),
+ DEFINE_OUTPUT( m_outputOnPlayerSpawnAsScout, "OnPlayerSpawnAsScout" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSpawnAsSniper, "OnPlayerSpawnAsSniper" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSpawnAsSoldier, "OnPlayerSpawnAsSoldier" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSpawnAsDemoman, "OnPlayerSpawnAsDemoman" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSpawnAsMedic, "OnPlayerSpawnAsMedic" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSpawnAsHeavy, "OnPlayerSpawnAsHeavy" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSpawnAsPyro, "OnPlayerSpawnAsPyro" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSpawnAsSpy, "OnPlayerSpawnAsSpy" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSpawnAsEngineer, "OnPlayerSpawnAsEngineer" ),
+ DEFINE_OUTPUT( m_outputOnPlayerDied, "OnPlayerDied" ),
+ DEFINE_OUTPUT( m_outputOnBotDied, "OnBotDied" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSwappedToWeaponSlotPrimary, "OnPlayerSwappedToPrimary" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSwappedToWeaponSlotSecondary, "OnPlayerSwappedToSecondary" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSwappedToWeaponSlotMelee, "OnPlayerSwappedToMelee" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSwappedToWeaponSlotBuilding, "OnPlayerSwappedToBuilding" ),
+ DEFINE_OUTPUT( m_outputOnPlayerSwappedToWeaponSlotPDA, "OnPlayerSwappedToPDA" ),
+ DEFINE_OUTPUT( m_outputOnPlayerBuiltOutsideSuggestedArea, "OnBuildOutsideArea" ),
+ DEFINE_OUTPUT( m_outputOnPlayerDetonateBuilding, "OnPlayerDetonateBuilding" ),
+END_DATADESC()
+
+LINK_ENTITY_TO_CLASS( tf_logic_training_mode, CTrainingModeLogic );
+
+void CTrainingModeLogic::SetupOnRoundStart()
+{
+ m_objText[0] = 0;
+ SetTrainingMsg( "" );
+}
+
+void CTrainingModeLogic::SetTrainingMsg(const char *msg)
+{
+ CBroadcastRecipientFilter allusers;
+ allusers.MakeReliable();
+ UserMessageBegin( allusers, "TrainingMsg" );
+ WRITE_STRING( msg );
+ MessageEnd();
+}
+
+void CTrainingModeLogic::SetTrainingObjective(const char *text)
+{
+ CBroadcastRecipientFilter allusers;
+ allusers.MakeReliable();
+ UserMessageBegin( allusers, "TrainingObjective" );
+ WRITE_STRING( text );
+ MessageEnd();
+}
+
+void CTrainingModeLogic::OnPlayerSpawned( CTFPlayer* pPlayer )
+{
+ if ( pPlayer->GetDesiredPlayerClassIndex() == TF_CLASS_UNDEFINED )
+ {
+ return;
+ }
+ if ( pPlayer->IsFakeClient() )
+ {
+ return;
+ }
+ int iClass = pPlayer->GetPlayerClass()->GetClassIndex();
+ switch ( iClass )
+ {
+ case TF_CLASS_SCOUT: m_outputOnPlayerSpawnAsScout.FireOutput( this, this ); break;
+ case TF_CLASS_SNIPER: m_outputOnPlayerSpawnAsSniper.FireOutput( this, this ); break;
+ case TF_CLASS_SOLDIER: m_outputOnPlayerSpawnAsSoldier.FireOutput( this, this ); break;
+ case TF_CLASS_DEMOMAN: m_outputOnPlayerSpawnAsDemoman.FireOutput( this, this ); break;
+ case TF_CLASS_MEDIC: m_outputOnPlayerSpawnAsMedic.FireOutput( this, this ); break;
+ case TF_CLASS_HEAVYWEAPONS: m_outputOnPlayerSpawnAsHeavy.FireOutput( this, this ); break;
+ case TF_CLASS_PYRO: m_outputOnPlayerSpawnAsPyro.FireOutput( this, this ); break;
+ case TF_CLASS_SPY: m_outputOnPlayerSpawnAsSpy.FireOutput( this, this ); break;
+ case TF_CLASS_ENGINEER: m_outputOnPlayerSpawnAsEngineer.FireOutput( this, this ); break;
+ }
+}
+
+void CTrainingModeLogic::OnPlayerDied( CTFPlayer *pPlayer, CBaseEntity *pKiller )
+{
+ m_outputOnPlayerDied.FireOutput( pKiller, this );
+}
+
+void CTrainingModeLogic::OnBotDied( CTFPlayer *pPlayer, CBaseEntity *pKiller )
+{
+ m_outputOnBotDied.FireOutput( pKiller, this );
+}
+
+void CTrainingModeLogic::OnPlayerSwitchedWeapons( CTFPlayer *pPlayer )
+{
+ CTFWeaponBase *pWeapon = (CTFWeaponBase*)pPlayer->GetActiveWeapon();
+ if ( pWeapon == NULL )
+ {
+ return;
+ }
+ switch ( pWeapon->GetTFWpnData().m_iWeaponType )
+ {
+ case TF_WPN_TYPE_PRIMARY: m_outputOnPlayerSwappedToWeaponSlotPrimary.FireOutput( this, this ); break;
+ case TF_WPN_TYPE_SECONDARY: m_outputOnPlayerSwappedToWeaponSlotSecondary.FireOutput( this, this ); break;
+ case TF_WPN_TYPE_MELEE: m_outputOnPlayerSwappedToWeaponSlotMelee.FireOutput( this, this ); break;
+ case TF_WPN_TYPE_BUILDING: m_outputOnPlayerSwappedToWeaponSlotBuilding.FireOutput( this, this ); break;
+ case TF_WPN_TYPE_PDA: m_outputOnPlayerSwappedToWeaponSlotPDA.FireOutput( this, this ); break;
+ }
+}
+
+void CTrainingModeLogic::OnPlayerWantsToContinue()
+{
+ if ( m_waitingForKeypressTimer.Get() != NULL )
+ {
+ m_waitingForKeypressTimer->FireNamedOutput( "OnTimer", variant_t(), this, this );
+ m_waitingForKeypressTimer = NULL;
+ TFGameRules()->SetIsWaitingForTrainingContinue( false );
+ }
+}
+
+void CTrainingModeLogic::OnPlayerBuiltBuilding( CTFPlayer *pPlayer, CBaseObject *pBaseObject )
+{
+ if ( pBaseObject && NotifyObjectBuiltInSuggestedArea( *pBaseObject ) == false )
+ {
+ m_outputOnPlayerBuiltOutsideSuggestedArea.FireOutput( pBaseObject, this );
+ }
+}
+
+void CTrainingModeLogic::OnPlayerUpgradedBuilding( CTFPlayer *pPlayer, CBaseObject *pBaseObject )
+{
+ if ( pBaseObject )
+ {
+ NotifyObjectUpgradedInSuggestedArea( *pBaseObject );
+ }
+}
+
+void CTrainingModeLogic::OnPlayerDetonateBuilding( CTFPlayer *pPlayer, CBaseObject *pBaseObject )
+{
+ m_outputOnPlayerDetonateBuilding.FireOutput( pPlayer, pBaseObject );
+}
+
+void CTrainingModeLogic::UpdateHUDObjective()
+{
+ if ( m_objText[0] != 0 )
+ {
+ SetTrainingObjective( m_objText );
+ }
+ else
+ {
+ SetTrainingObjective("");
+ }
+}
+
+const char* CTrainingModeLogic::GetNextMap()
+{
+ return m_nextMapName.ToCStr();
+}
+
+const char* CTrainingModeLogic::GetTrainingEndText()
+{
+ return m_endTrainingText.ToCStr();
+}
+
+int CTrainingModeLogic::GetDesiredClass() const
+{
+ return training_class.GetInt();
+}
+
+void CTrainingModeLogic::InputForcePlayerSpawnAsClassOutput( inputdata_t &inputdata )
+{
+ // This is a bit weird, but we will call this for every player--bots should be ignored
+ CTFPlayer *pPlayer;
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+
+ if ( !pPlayer )
+ continue;
+ OnPlayerSpawned( pPlayer );
+ }
+}
+
+void CTrainingModeLogic::InputKickAllBots( inputdata_t &inputdata )
+{
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( pPlayer && pPlayer->IsFakeClient() )
+ {
+ engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", pPlayer->GetUserID() ) );
+ }
+ }
+}
+
+void CTrainingModeLogic::InputShowTrainingMsg( inputdata_t &inputdata )
+{
+ if ( TFGameRules()->IsInTraining() )
+ {
+ SetTrainingMsg( inputdata.value.String() );
+ }
+}
+
+void CTrainingModeLogic::InputShowTrainingObjective( inputdata_t &inputdata )
+{
+ if ( !TFGameRules()->IsInTraining() )
+ return;
+
+ //First try to find the unicode string to send over.
+ wchar_t *strPtr = NULL;
+ strPtr = g_pVGuiLocalize->Find( inputdata.value.String() );
+
+ if (NULL == strPtr)
+ {
+ V_strcpy_safe(m_objText, inputdata.value.String());
+ }
+ else
+ {
+ g_pVGuiLocalize->ConvertUnicodeToANSI(strPtr, m_objText, kMaxLengthObjectiveText);
+ }
+
+ UpdateHUDObjective();
+}
+
+void CTrainingModeLogic::InputShowTrainingHUD( inputdata_t &inputdata )
+{
+ if ( !TFGameRules()->IsInTraining() )
+ return;
+ TFGameRules()->SetTrainingHUDVisible( true );
+}
+
+void CTrainingModeLogic::InputHideTrainingHUD( inputdata_t &inputdata )
+{
+ if ( !TFGameRules()->IsInTraining() )
+ return;
+ TFGameRules()->SetTrainingHUDVisible( false );
+}
+
+void CTrainingModeLogic::InputEndTraining( inputdata_t &inputdata )
+{
+ if ( !TFGameRules()->IsInTraining() )
+ return;
+
+ TFGameRules()->SetAllowTrainingAchievements( true );
+
+ m_endTrainingText = inputdata.value.StringID();
+
+ CTFPlayer* pHumanPlayer = ToTFPlayer( UTIL_GetListenServerHost() );
+
+ if (NULL == pHumanPlayer) return;
+
+ int iTeam = pHumanPlayer->GetTeamNumber();
+
+ bool force_map_reset = true;
+ CTeamplayRoundBasedRules *pGameRules = dynamic_cast<CTeamplayRoundBasedRules *>( GameRules() );
+ pGameRules->SetWinningTeam( iTeam, pGameRules->GetWinReason(), force_map_reset );
+
+ // Show a training win screen so send that event instead.
+ IGameEvent *winEvent = gameeventmanager->CreateEvent( "training_complete" );
+ if ( winEvent )
+ {
+ winEvent->SetString( "map", STRING( gpGlobals->mapname ) );
+ winEvent->SetString( "next_map", GetNextMap() );
+ winEvent->SetString( "text", GetTrainingEndText() );
+
+ gameeventmanager->FireEvent( winEvent );
+ }
+}
+
+void CTrainingModeLogic::InputPlaySoundOnPlayer( inputdata_t &inputdata )
+{
+ if ( !TFGameRules()->IsInTraining() )
+ return;
+ CTFPlayer* pHumanPlayer = ToTFPlayer( UTIL_GetListenServerHost() );
+
+ if (NULL == pHumanPlayer)
+ return;
+
+ pHumanPlayer->EmitSound( inputdata.value.String() );
+}
+
+void CTrainingModeLogic::InputWaitForTimerOrKeypress( inputdata_t &inputdata )
+{
+ if ( !TFGameRules()->IsInTraining() )
+ return;
+
+ m_waitingForKeypressTimer = gEntList.FindEntityByName( NULL, inputdata.value.String() );
+ TFGameRules()->SetIsWaitingForTrainingContinue( m_waitingForKeypressTimer.Get() != NULL );
+}
+
+void CTrainingModeLogic::InputSetNextMap( inputdata_t &inputdata )
+{
+ m_nextMapName = AllocPooledString( inputdata.value.String() );
+}
+
+void CTrainingModeLogic::InputForcePlayerSwapToWeapon( inputdata_t &inputdata )
+{
+ CTFPlayer* pHumanPlayer = ToTFPlayer( UTIL_GetListenServerHost() );
+
+ if (NULL == pHumanPlayer)
+ return;
+
+ CBaseCombatWeapon *pWeapon = NULL;
+
+ if ( FStrEq( inputdata.value.String(), "primary" ) )
+ {
+ pWeapon = pHumanPlayer->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
+ }
+ else if ( FStrEq( inputdata.value.String(), "secondary" ) )
+ {
+ pWeapon = pHumanPlayer->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY );
+ }
+ else if ( FStrEq( inputdata.value.String(), "melee" ) )
+ {
+ pWeapon = pHumanPlayer->Weapon_GetSlot( TF_WPN_TYPE_MELEE );
+ }
+ else if ( FStrEq( inputdata.value.String(), "grenade" ) )
+ {
+ pWeapon = pHumanPlayer->Weapon_GetSlot( TF_WPN_TYPE_GRENADE );
+ }
+ else if ( FStrEq( inputdata.value.String(), "building" ) )
+ {
+ pWeapon = pHumanPlayer->Weapon_GetSlot( TF_WPN_TYPE_BUILDING );
+ }
+ else if ( FStrEq( inputdata.value.String(), "pda" ) )
+ {
+ pWeapon = pHumanPlayer->Weapon_GetSlot( TF_WPN_TYPE_PDA );
+ }
+ else if ( FStrEq( inputdata.value.String(), "item1" ) )
+ {
+ pWeapon = pHumanPlayer->Weapon_GetSlot( TF_WPN_TYPE_ITEM1 );
+ }
+ else if ( FStrEq( inputdata.value.String(), "item2" ) )
+ {
+ pWeapon = pHumanPlayer->Weapon_GetSlot( TF_WPN_TYPE_ITEM2 );
+ }
+
+ if ( pWeapon )
+ {
+ pHumanPlayer->Weapon_Switch( pWeapon );
+ }
+
+}
+
+LINK_ENTITY_TO_CLASS( tf_logic_multiple_escort, CMultipleEscort );
+LINK_ENTITY_TO_CLASS( tf_logic_hybrid_ctf_cp, CHybridMap_CTF_CP );
+LINK_ENTITY_TO_CLASS( tf_logic_medieval, CMedievalLogic );
+
+
+BEGIN_DATADESC(CTFHolidayEntity)
+ DEFINE_KEYFIELD( m_nHolidayType, FIELD_INTEGER, "holiday_type" ),
+ DEFINE_KEYFIELD( m_nTauntInHell, FIELD_INTEGER, "tauntInHell" ),
+ DEFINE_KEYFIELD( m_nAllowHaunting, FIELD_INTEGER, "allowHaunting" ),
+
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "HalloweenSetUsingSpells", InputHalloweenSetUsingSpells ),
+ DEFINE_INPUTFUNC( FIELD_STRING, "Halloween2013TeleportToHell", InputHalloweenTeleportToHell ),
+END_DATADESC();
+
+LINK_ENTITY_TO_CLASS( tf_logic_holiday, CTFHolidayEntity );
+
+
+void CTFHolidayEntity::InputHalloweenSetUsingSpells( inputdata_t &inputdata )
+{
+ if ( !TFGameRules() )
+ return;
+
+ TFGameRules()->SetUsingSpells( ( inputdata.value.Int() == 0 ) ? false : true );
+}
+
+void CTFHolidayEntity::InputHalloweenTeleportToHell( inputdata_t &inputdata )
+{
+ m_nWinningTeam = FStrEq( "red", inputdata.value.String() ) ? TF_TEAM_RED : TF_TEAM_BLUE;
+
+ CUtlVector< CTFPlayer * > vecPlayers;
+ CollectPlayers( &vecPlayers, TEAM_ANY, false );
+
+ FOR_EACH_VEC( vecPlayers, i )
+ {
+ CTFPlayer *pPlayer = vecPlayers[i];
+ // Only do these effects if the player is alive
+ if ( !pPlayer->IsAlive() )
+ continue;
+
+ // Fade to white
+ color32 fadeColor = {255,255,255,255};
+ UTIL_ScreenFade( pPlayer, fadeColor, 2.f, 0.5, FFADE_OUT | FFADE_PURGE );
+
+ // Do a zoom in effect
+ pPlayer->SetFOV( pPlayer, 10.f, 2.5f, 0.f );
+ // Rumble like something important happened
+ UTIL_ScreenShake(pPlayer->GetAbsOrigin(), 100.f, 150, 4.f, 0.f, SHAKE_START, true );
+ }
+
+ // Play a sound for all players
+ TFGameRules()->BroadcastSound( 255, "Halloween.hellride" );
+
+ SetContextThink( &CTFHolidayEntity::Teleport, gpGlobals->curtime + 2.5f, "TeleportToHell" );
+}
+
+void CTFHolidayEntity::Teleport()
+{
+ RemoveAll2013HalloweenTeleportSpellsInMidFlight();
+
+ const char *pszRedString = ( m_nWinningTeam == TF_TEAM_RED ) ? "winner" : "loser";
+ const char *pszBlueString = ( m_nWinningTeam == TF_TEAM_BLUE ) ? "winner" : "loser";
+
+ CUtlVector< CTFPlayer* > vecTeleportedPlayers;
+ TFGameRules()->TeleportPlayersToTargetEntities( TF_TEAM_RED, CFmtStr( "spawn_loot_%s" , pszRedString ), &vecTeleportedPlayers );
+ TFGameRules()->TeleportPlayersToTargetEntities( TF_TEAM_BLUE, CFmtStr( "spawn_loot_%s" , pszBlueString ), &vecTeleportedPlayers );
+
+ // clear dancer
+ m_vecDancers.RemoveAll();
+
+ // remove players' projectiles and buildings from world
+ TFGameRules()->RemoveAllProjectilesAndBuildings();
+
+ FOR_EACH_VEC( vecTeleportedPlayers, i )
+ {
+ CTFPlayer *pPlayer = vecTeleportedPlayers[i];
+
+ // Roll a new, low-tier spell
+ CTFSpellBook *pSpellBook = dynamic_cast< CTFSpellBook* >( pPlayer->GetEntityForLoadoutSlot( LOADOUT_POSITION_ACTION ) );
+ if ( pSpellBook )
+ {
+ pSpellBook->ClearSpell();
+ if ( TFGameRules()->IsHalloweenScenario( CTFGameRules::HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ pSpellBook->RollNewSpell( 0, true );
+ }
+ }
+
+ // Do a zoom effect
+ pPlayer->SetFOV( pPlayer, tf_teleporter_fov_start.GetInt() );
+ pPlayer->SetFOV( pPlayer, 0, 1.f, tf_teleporter_fov_start.GetInt() );
+
+ // Screen flash
+ color32 fadeColor = {255,255,255,100};
+ UTIL_ScreenFade( pPlayer, fadeColor, 0.25, 0.4, FFADE_IN );
+
+ const float flDanceTime = 6.f;
+
+ if ( ShouldTauntInHell() || ( TFGameRules()->GetHalloweenScenario() == CTFGameRules::HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ pPlayer->m_Shared.AddCond( TF_COND_HALLOWEEN_THRILLER, flDanceTime );
+ }
+
+ pPlayer->m_Shared.AddCond( TF_COND_HALLOWEEN_IN_HELL );
+
+ // Losers get their health set to max. Winners get overhealed
+ bool bIsWinner = ( pPlayer->GetTeamNumber() == m_nWinningTeam );
+ float flMax = bIsWinner ? ( pPlayer->GetMaxHealth() * 1.6f ) : ( pPlayer->GetMaxHealth() * 1.1 );
+ float flToHeal = flMax - pPlayer->GetHealth();
+ // Overheal the winning team, and just restore the losing team to full health
+ pPlayer->m_Shared.Heal( pPlayer, flToHeal / flDanceTime, bIsWinner ? 1.5f : 1.f, 1.0f );
+
+ // Give them full ammo
+ pPlayer->GiveAmmo( 1000, TF_AMMO_PRIMARY );
+ pPlayer->GiveAmmo( 1000, TF_AMMO_SECONDARY );
+ pPlayer->GiveAmmo( 1000, TF_AMMO_METAL );
+ pPlayer->GiveAmmo( 1000, TF_AMMO_GRENADES1 );
+ pPlayer->GiveAmmo( 1000, TF_AMMO_GRENADES2 );
+ pPlayer->GiveAmmo( 1000, TF_AMMO_GRENADES3 );
+
+ // Refills weapon clips, too
+ for ( int i = 0; i < MAX_WEAPONS; i++ )
+ {
+ CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase* >( pPlayer->GetWeapon( i ) );
+ if ( !pWeapon )
+ continue;
+
+ pWeapon->GiveDefaultAmmo();
+
+ if ( pWeapon->IsEnergyWeapon() )
+ {
+ pWeapon->WeaponRegenerate();
+ }
+ }
+
+ m_vecDancers.AddToTail( pPlayer );
+ }
+
+ // Set this flag. Lets us check elsewhere if it's hell time
+ if ( TFGameRules() )
+ {
+ TFGameRules()->SetPlayersInHell( true );
+ }
+
+ if ( ShouldTauntInHell() || ( TFGameRules()->GetHalloweenScenario() == CTFGameRules::HALLOWEEN_SCENARIO_HIGHTOWER ) )
+ {
+ const float flDanceTime = 0.5f;
+ const float flDanceDuration = 2.75f;
+
+ SetContextThink( &CTFHolidayEntity::HalloweenTeleportToHellDanceThink, gpGlobals->curtime + flDanceTime, "DanceThink1" );
+ SetContextThink( &CTFHolidayEntity::HalloweenTeleportToHellDanceThink, gpGlobals->curtime + flDanceTime + flDanceDuration, "DanceThink2" );
+ }
+}
+
+
+void CTFHolidayEntity::HalloweenTeleportToHellDanceThink( void )
+{
+ FOR_EACH_VEC( m_vecDancers, i )
+ {
+ CTFPlayer* pPlayer = m_vecDancers[i];
+ if ( !pPlayer )
+ continue;
+
+ // Dance
+ pPlayer->Taunt();
+ }
+}
+
+void CTFHolidayEntity::FireGameEvent( IGameEvent *event )
+{
+ const char *eventName = event->GetName();
+
+#ifdef GAME_DLL
+ if ( !Q_strcmp( eventName, "player_turned_to_ghost" )
+ || !Q_strcmp( eventName, "player_disconnect" )
+ || !Q_strcmp( eventName, "player_team" ))
+ {
+ if ( TFGameRules()->ArePlayersInHell() )
+ {
+ CUtlVector< CTFPlayer * > vecPlayers;
+ CollectPlayers( &vecPlayers, TF_TEAM_RED, true );
+ CollectPlayers( &vecPlayers, TF_TEAM_BLUE, true, true );
+
+ FOR_EACH_VEC( vecPlayers, i )
+ {
+ // If everyone is a ghost
+ if ( !vecPlayers[i]->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) )
+ return;
+ }
+
+ // Everyone is a ghost. Stalemate!
+ TFGameRules()->SetWinningTeam( TEAM_UNASSIGNED, WINREASON_STALEMATE, true, false );
+ }
+ }
+#endif
+}
+
+BEGIN_DATADESC(CKothLogic)
+ DEFINE_KEYFIELD( m_nTimerInitialLength, FIELD_INTEGER, "timer_length" ),
+ DEFINE_KEYFIELD( m_nTimeToUnlockPoint, FIELD_INTEGER, "unlock_point" ),
+
+ DEFINE_INPUTFUNC( FIELD_VOID, "RoundSpawn", InputRoundSpawn ),
+ DEFINE_INPUTFUNC( FIELD_VOID, "RoundActivate", InputRoundActivate ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "SetRedTimer", InputSetRedTimer ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "SetBlueTimer", InputSetBlueTimer ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "AddRedTimer", InputAddRedTimer ),
+ DEFINE_INPUTFUNC( FIELD_INTEGER, "AddBlueTimer", InputAddBlueTimer ),
+END_DATADESC();
+
+LINK_ENTITY_TO_CLASS( tf_logic_koth, CKothLogic );
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CKothLogic::InputRoundSpawn( inputdata_t &input )
+{
+ if ( TFGameRules() && TFGameRules()->IsInKothMode() )
+ {
+ // create the koth team_round_timer entities
+ variant_t sVariant;
+ sVariant.SetInt( m_nTimerInitialLength );
+
+ CTeamRoundTimer *pTimer = NULL;
+
+ pTimer = (CTeamRoundTimer*)CBaseEntity::Create( "team_round_timer", vec3_origin, vec3_angle );
+ if ( pTimer )
+ {
+ TFGameRules()->SetKothTeamTimer( TF_TEAM_BLUE, pTimer );
+ pTimer->SetName( MAKE_STRING( "zz_blue_koth_timer" ) );
+ pTimer->SetShowInHud( true );
+ pTimer->AcceptInput( "SetTime", NULL, NULL, sVariant, 0 );
+ pTimer->AcceptInput( "Pause", NULL, NULL, sVariant, 0 );
+
+ m_hBlueTimer = pTimer;
+ }
+
+ pTimer = (CTeamRoundTimer*)CBaseEntity::Create( "team_round_timer", vec3_origin, vec3_angle );
+ if ( pTimer )
+ {
+ TFGameRules()->SetKothTeamTimer( TF_TEAM_RED, pTimer );
+ pTimer->SetName( MAKE_STRING( "zz_red_koth_timer" ) );
+ pTimer->SetShowInHud( true );
+ pTimer->AcceptInput( "SetTime", NULL, NULL, sVariant, 0 );
+ pTimer->AcceptInput( "Pause", NULL, NULL, sVariant, 0 );
+
+ m_hRedTimer = pTimer;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CKothLogic::InputRoundActivate( inputdata_t &inputdata )
+{
+ if ( TFGameRules() && TFGameRules()->IsInKothMode() )
+ {
+ CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL;
+ if ( pMaster )
+ {
+ variant_t sVariant;
+ sVariant.SetInt( m_nTimeToUnlockPoint );
+
+ for ( int i = 0 ; i < pMaster->GetNumPoints() ; i++ )
+ {
+ CTeamControlPoint *pPoint = pMaster->GetControlPoint( i );
+ if ( pPoint )
+ {
+ pPoint->AcceptInput( "SetLocked", NULL, NULL, sVariant, 0 );
+
+ if ( m_nTimeToUnlockPoint > 0 )
+ {
+ pPoint->AcceptInput( "SetUnlockTime", NULL, NULL, sVariant, 0 );
+ }
+ }
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CKothLogic::InputSetRedTimer( inputdata_t &inputdata )
+{
+ if ( TFGameRules() && TFGameRules()->IsInKothMode() )
+ {
+ if ( m_hRedTimer )
+ {
+ m_hRedTimer->SetTimeRemaining( inputdata.value.Int() );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CKothLogic::InputSetBlueTimer( inputdata_t &inputdata )
+{
+ if ( TFGameRules() && TFGameRules()->IsInKothMode() )
+ {
+ if ( m_hBlueTimer )
+ {
+ m_hBlueTimer->SetTimeRemaining( inputdata.value.Int() );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CKothLogic::InputAddRedTimer( inputdata_t &inputdata )
+{
+ if ( TFGameRules() && TFGameRules()->IsInKothMode() )
+ {
+ if ( m_hRedTimer )
+ {
+ m_hRedTimer->AddTimerSeconds( inputdata.value.Int() );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CKothLogic::InputAddBlueTimer( inputdata_t &inputdata )
+{
+ if ( TFGameRules() && TFGameRules()->IsInKothMode() )
+ {
+ if ( m_hBlueTimer )
+ {
+ m_hBlueTimer->AddTimerSeconds( inputdata.value.Int() );
+ }
+ }
+}
+
+BEGIN_DATADESC(CCPTimerLogic)
+ DEFINE_KEYFIELD( m_iszControlPointName, FIELD_STRING, "controlpoint" ),
+ DEFINE_KEYFIELD( m_nTimerLength, FIELD_INTEGER, "timer_length" ),
+
+ DEFINE_INPUTFUNC( FIELD_VOID, "RoundSpawn", InputRoundSpawn ),
+
+ DEFINE_OUTPUT( m_onCountdownStart, "OnCountdownStart" ),
+ DEFINE_OUTPUT( m_onCountdown15SecRemain, "OnCountdown15SecRemain" ),
+ DEFINE_OUTPUT( m_onCountdown10SecRemain, "OnCountdown10SecRemain" ),
+ DEFINE_OUTPUT( m_onCountdown5SecRemain, "OnCountdown5SecRemain" ),
+ DEFINE_OUTPUT( m_onCountdownEnd, "OnCountdownEnd" ),
+END_DATADESC();
+
+LINK_ENTITY_TO_CLASS( tf_logic_cp_timer, CCPTimerLogic );
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCPTimerLogic::InputRoundSpawn( inputdata_t &input )
+{
+ if ( m_iszControlPointName != NULL_STRING )
+ {
+ // We need to re-find our control point, because they're recreated over round restarts
+ m_hControlPoint = dynamic_cast<CTeamControlPoint*>( gEntList.FindEntityByName( NULL, m_iszControlPointName ) );
+ if ( !m_hControlPoint )
+ {
+ Warning( "%s failed to find control point named '%s'\n", GetClassname(), STRING(m_iszControlPointName) );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CCPTimerLogic::TimerMayExpire( void )
+{
+ if ( m_hControlPoint )
+ {
+ if ( TeamplayGameRules()->TeamMayCapturePoint( TF_TEAM_BLUE, m_hControlPoint->GetPointIndex() ) )
+ return false;
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCPTimerLogic::Think( void )
+{
+ if ( !TFGameRules() || !ObjectiveResource() )
+ return;
+
+ if ( TFGameRules()->State_Get() == GR_STATE_TEAM_WIN )
+ {
+ // game has already been won, our job is done
+ m_pointTimer.Invalidate();
+ SetContextThink( &CCPTimerLogic::Think, gpGlobals->curtime + 0.15, CP_TIMER_THINK );
+ }
+
+ if ( m_hControlPoint )
+ {
+ if ( TeamplayGameRules()->TeamMayCapturePoint( TF_TEAM_BLUE, m_hControlPoint->GetPointIndex() ) )
+ {
+ if ( !m_pointTimer.HasStarted() )
+ {
+ m_pointTimer.Start( m_nTimerLength );
+ m_onCountdownStart.FireOutput( this, this );
+
+ ObjectiveResource()->SetCPTimerTime( m_hControlPoint->GetPointIndex(), gpGlobals->curtime + m_nTimerLength );
+ }
+ else
+ {
+ if ( m_pointTimer.IsElapsed() )
+ {
+ // the point must be fully owned by the owner before we reset
+ if ( ObjectiveResource()->GetCappingTeam( m_hControlPoint->GetPointIndex() ) == TEAM_UNASSIGNED )
+ {
+ m_pointTimer.Invalidate();
+ m_onCountdownEnd.FireOutput( this, this );
+ m_bFire15SecRemain = m_bFire10SecRemain = m_bFire5SecRemain = true;
+
+ ObjectiveResource()->SetCPTimerTime( m_hControlPoint->GetPointIndex(), -1.0f );
+ }
+ }
+ else
+ {
+ float flRemainingTime = m_pointTimer.GetRemainingTime();
+
+ if ( flRemainingTime <= 15.0f && m_bFire15SecRemain )
+ {
+ m_bFire15SecRemain = false;
+ }
+ else if ( flRemainingTime <= 10.0f && m_bFire10SecRemain )
+ {
+ m_bFire10SecRemain = false;
+ }
+ else if ( flRemainingTime <= 5.0f && m_bFire5SecRemain )
+ {
+ m_bFire5SecRemain = false;
+ }
+ }
+ }
+ }
+ else
+ {
+ m_pointTimer.Invalidate();
+ }
+ }
+
+ SetContextThink( &CCPTimerLogic::Think, gpGlobals->curtime + 0.15, CP_TIMER_THINK );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::AddPlayerToQueue( CTFPlayer *pPlayer )
+{
+ //Already in Queue
+ if ( m_hArenaPlayerQueue.Find( pPlayer ) != m_hArenaPlayerQueue.InvalidIndex() )
+ return;
+
+ if ( pPlayer->IsArenaSpectator() == true )
+ return;
+
+// Msg( "AddPlayerToQueue:: Adding to queue: %s\n", pPlayer->GetPlayerName() );
+
+ m_hArenaPlayerQueue.AddToTail( pPlayer );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::AddPlayerToQueueHead( CTFPlayer *pPlayer )
+{
+ //Already in Queue
+ if ( m_hArenaPlayerQueue.Find( pPlayer ) != m_hArenaPlayerQueue.InvalidIndex() )
+ return;
+
+ m_hArenaPlayerQueue.AddToHead( pPlayer );
+
+// Msg( "AddPlayerToQueueHead:: Adding to queue: %s\n", pPlayer->GetPlayerName() );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::RemovePlayerFromQueue( CTFPlayer *pPlayer )
+{
+ //Not in list?
+ if ( m_hArenaPlayerQueue.Find( pPlayer ) == m_hArenaPlayerQueue.InvalidIndex() )
+ return;
+
+ m_hArenaPlayerQueue.FindAndRemove( pPlayer );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::OnNavMeshLoad( void )
+{
+ TheNavMesh->SetPlayerSpawnName( "info_player_teamspawn" );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::OnDispenserBuilt( CBaseEntity *dispenser )
+{
+ if ( !m_healthVector.HasElement( dispenser ) )
+ {
+ m_healthVector.AddToTail( dispenser );
+ }
+
+ if ( !m_ammoVector.HasElement( dispenser ) )
+ {
+ m_ammoVector.AddToTail( dispenser );
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::OnDispenserDestroyed( CBaseEntity *dispenser )
+{
+ m_healthVector.FindAndFastRemove( dispenser );
+ m_ammoVector.FindAndFastRemove( dispenser );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+CPhysicsProp *CreateBeachBall( const Vector &vSpawnPos, const QAngle &qSpawnAngles );
+CPhysicsProp *CreateSoccerBall( const Vector &vSpawnPos, const QAngle &qSpawnAngles );
+
+#ifdef STAGING_ONLY
+//------------------------------------------------------------------------------
+// Purpose:
+//------------------------------------------------------------------------------
+void CC_Spawn_SoccerBall( const CCommand& args )
+{
+ CBasePlayer *pPlayer = UTIL_GetCommandClient();
+ if ( pPlayer )
+ {
+ trace_t tr;
+ Vector forward;
+ pPlayer->EyeVectors( &forward );
+ UTIL_TraceLine( pPlayer->EyePosition(),
+ pPlayer->EyePosition() + forward * MAX_TRACE_LENGTH,MASK_NPCSOLID,
+ pPlayer, COLLISION_GROUP_NONE, &tr );
+ if ( tr.fraction != 1.0 )
+ {
+ CreateSoccerBall( tr.endpos, vec3_angle );
+ }
+ }
+}
+
+ConCommand tf_spawn_soccerball( "tf_spawn_soccerball", CC_Spawn_SoccerBall, "", FCVAR_CHEAT );
+#endif // STAGING_ONLY
+
+static bool CanFindBallSpawnLocation( const Vector& vSearchOrigin, Vector *out_pvDropSpot )
+{
+ // find clear space to drop the ball
+ for( float angle = 0.0f; angle < 2.0f * M_PI; angle += 0.2f )
+ {
+ Vector forward;
+ FastSinCos( angle, &forward.y, &forward.x );
+ forward.z = 0.0f;
+
+ const float ballRadius = 16.0f;
+ const float playerRadius = 20.0f;
+
+ Vector hullMins( -ballRadius, -ballRadius, -ballRadius );
+ Vector hullMaxs( ballRadius, ballRadius, ballRadius );
+
+ Vector dropSpot = vSearchOrigin + 1.2f * ( playerRadius + ballRadius ) * forward;
+
+ trace_t result;
+ UTIL_TraceHull( dropSpot, dropSpot, hullMins, hullMaxs, MASK_PLAYERSOLID, NULL, &result );
+
+ if ( !result.DidHit() )
+ {
+ *out_pvDropSpot = dropSpot;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CTFGameRules::OnPlayerSpawned( CTFPlayer *pPlayer )
+{
+ // coach?
+ CSteamID steamIDForPlayer;
+ if ( pPlayer->GetSteamID( &steamIDForPlayer ) )
+ {
+ // find out if we are supposed to coach
+ int idx = m_mapCoachToStudentMap.Find( steamIDForPlayer.GetAccountID() );
+ if ( m_mapCoachToStudentMap.IsValidIndex( idx ) )
+ {
+ // find student
+ uint32 studentAccountID = m_mapCoachToStudentMap[idx];
+ CSteamID steamIDForStudent;
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CTFPlayer *pPotentialStudent = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( NULL == pPotentialStudent )
+ {
+ continue;
+ }
+ // is this the student?
+ if ( pPotentialStudent->GetSteamID( &steamIDForStudent ) && steamIDForStudent.GetAccountID() == studentAccountID )
+ {
+ Coaching_Start( pPlayer, pPotentialStudent );
+ // @todo (Tom Bui): Not sure this is required--nothing seems to use it
+ // engine->ClientCommand( pPlayer->edict(), "cl_spec_mode %d", OBS_MODE_IN_EYE );
+ // finally, notify the GC
+ GCSDK::CProtoBufMsg< CMsgTFCoaching_CoachJoined > msg( k_EMsgGCCoaching_CoachJoined );
+ msg.Body().set_account_id_coach( steamIDForPlayer.GetAccountID() );
+ GCClientSystem()->BSendMessage( msg );
+ break;
+ }
+ }
+ // remove from the map now so the coach can join later as a normal player if they DC
+ m_mapCoachToStudentMap.RemoveAt( idx );
+ }
+ }
+ // warp coach to student?
+ if ( pPlayer->GetCoach() )
+ {
+ // warp the coach to student
+ pPlayer->GetCoach()->SetObserverTarget( pPlayer );
+ pPlayer->GetCoach()->StartObserverMode( OBS_MODE_CHASE );
+ }
+
+ // notify training
+ if ( m_hTrainingModeLogic )
+ {
+ m_hTrainingModeLogic->OnPlayerSpawned( pPlayer );
+ }
+
+#ifdef GAME_DLL
+ if ( !IsInTraining() )
+ {
+ // Birthday beachball ball spawning.
+ if ( IsBirthday() &&
+ !m_hasSpawnedToy &&
+ pPlayer->GetTeamNumber() == TF_TEAM_BLUE && // always give ball to first blue player, since they are often trapped during setup
+ RandomInt( 0, 100 ) < tf_birthday_ball_chance.GetInt() )
+ {
+ Vector vDropSpot;
+ if ( CanFindBallSpawnLocation( pPlayer->WorldSpaceCenter(), &vDropSpot ) )
+ {
+ CPhysicsProp *ball = CreateBeachBall( vDropSpot, pPlayer->GetAbsAngles() );
+ if ( ball )
+ {
+ m_hasSpawnedToy = true;
+
+ // turn on the birthday skin
+ ball->m_nSkin = 1;
+ }
+ }
+ }
+
+ // Soccer ball spawning if wearing soccer cleats.
+ if ( !m_bHasSpawnedSoccerBall[ pPlayer->GetTeamNumber() ] )
+ {
+ enum
+ {
+ kSpawnWith_Nothing = 0,
+ kSpawnWith_SoccerBall = 1,
+ };
+
+ int iSpawnWithPhysicsToy = kSpawnWith_Nothing;
+ CALL_ATTRIB_HOOK_INT_ON_OTHER( pPlayer, iSpawnWithPhysicsToy, spawn_with_physics_toy );
+ if ( iSpawnWithPhysicsToy == kSpawnWith_SoccerBall )
+ {
+ Vector vDropSpot;
+ if ( CanFindBallSpawnLocation( pPlayer->WorldSpaceCenter(), &vDropSpot ) )
+ {
+ CPhysicsProp *ball = CreateSoccerBall( vDropSpot, pPlayer->GetAbsAngles() );
+ if ( ball )
+ {
+ m_bHasSpawnedSoccerBall[ pPlayer->GetTeamNumber() ] = true;
+
+ // turn on the birthday skin
+ ball->m_nSkin = pPlayer->GetTeamNumber() == TF_TEAM_BLUE ? 1 : 0;
+ }
+ }
+ }
+ }
+ }
+#endif
+}
+
+
+class CGCCoaching_CoachJoining : public GCSDK::CGCClientJob
+{
+public:
+ CGCCoaching_CoachJoining( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {}
+
+ virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg< CMsgTFCoaching_CoachJoining > msg( pNetPacket );
+ if ( TFGameRules() )
+ {
+ TFGameRules()->OnCoachJoining( msg.Body().account_id_coach(), msg.Body().account_id_student() );
+ }
+ return true;
+ }
+};
+GC_REG_JOB( GCSDK::CGCClient, CGCCoaching_CoachJoining, "CGCCoaching_CoachJoining", k_EMsgGCCoaching_CoachJoining, GCSDK::k_EServerTypeGCClient );
+
+class CGCCoaching_RemoveCurrentCoach : public GCSDK::CGCClientJob
+{
+public:
+ CGCCoaching_RemoveCurrentCoach( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {}
+
+ virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg< CMsgTFCoaching_RemoveCurrentCoach > msg( pNetPacket );
+ if ( TFGameRules() )
+ {
+ TFGameRules()->OnRemoveCoach( msg.Body().account_id_coach() );
+ }
+ return true;
+ }
+};
+GC_REG_JOB( GCSDK::CGCClient, CGCCoaching_RemoveCurrentCoach, "CGCCoaching_RemoveCurrentCoach", k_EMsgGCCoaching_RemoveCurrentCoach, GCSDK::k_EServerTypeGCClient );
+
+class CGCUseServerModificationItemJob : public GCSDK::CGCClientJob
+{
+public:
+ CGCUseServerModificationItemJob( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {}
+
+ virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg<CMsgGC_GameServer_UseServerModificationItem> msg( pNetPacket );
+
+ // If this server doesn't have the capability to call a vote right now for whatever reason, we
+ // give up and return immediate failure to the GC. If the vote gets called, we'll send up pass/fail
+ // when it finishes.
+ if ( !g_voteController || !g_voteController->CreateVote( DEDICATED_SERVER, "eternaween", "" ) )
+ {
+ GCSDK::CProtoBufMsg<CMsgGC_GameServer_UseServerModificationItem_Response> msgResponse( k_EMsgGC_GameServer_UseServerModificationItem_Response );
+ msgResponse.Body().set_server_response_code( CMsgGC_GameServer_UseServerModificationItem_Response::kServerModificationItemServerResponse_NoVoteCalled );
+ m_pGCClient->BSendMessage( msgResponse );
+ }
+
+ return true;
+ }
+};
+GC_REG_JOB( GCSDK::CGCClient, CGCUseServerModificationItemJob, "CGCUseServerModificationItemJob", k_EMsgGC_GameServer_UseServerModificationItem, GCSDK::k_EServerTypeGCClient );
+
+class CGCUpdateServerModificationItemStateJob : public GCSDK::CGCClientJob
+{
+public:
+ CGCUpdateServerModificationItemStateJob( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {}
+
+ virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg<CMsgGC_GameServer_ServerModificationItem> msg( pNetPacket );
+
+ switch ( msg.Body().modification_type() )
+ {
+ case kGameServerModificationItem_Halloween:
+ tf_item_based_forced_holiday.SetValue( msg.Body().active() ? kHoliday_Halloween : kHoliday_None );
+ g_fEternaweenAutodisableTime = engine->Time() + (SERVER_MODIFICATION_ITEM_DURATION_IN_MINUTES * 60.0f);
+ if ( TFGameRules() )
+ {
+ TFGameRules()->FlushAllAttributeCaches();
+ }
+ break;
+ default:
+ Warning( "%s: unknown modification type %u for server item.\n", __FUNCTION__, msg.Body().modification_type() );
+ break;
+ }
+
+ return true;
+ }
+};
+GC_REG_JOB( GCSDK::CGCClient, CGCUpdateServerModificationItemStateJob, "CGCUpdateServerModificationItemStateJob", k_EMsgGC_GameServer_ModificationItemState, GCSDK::k_EServerTypeGCClient );
+
+#ifdef _DEBUG
+CON_COMMAND( coaching_stop, "Stop coaching" )
+{
+ CTFPlayer* pCoach = ToTFPlayer( UTIL_GetListenServerHost() );
+ Coaching_Stop( pCoach );
+}
+
+CON_COMMAND( coaching_remove_coach, "Remove current coach" )
+{
+ CTFPlayer* pStudent = ToTFPlayer( UTIL_GetListenServerHost() );
+ CTFPlayer *pCoach = pStudent->GetCoach();
+ if ( pCoach )
+ {
+ Coaching_Stop( pCoach );
+ }
+}
+
+CON_COMMAND( coaching_force_coach, "Force self as coach" )
+{
+ CTFPlayer* pCoachPlayer = ToTFPlayer( UTIL_GetListenServerHost() );
+ if ( pCoachPlayer && TFGameRules() )
+ {
+ for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ )
+ {
+ CTFPlayer *pStudentPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( pStudentPlayer != pCoachPlayer )
+ {
+ Coaching_Start( pCoachPlayer, pStudentPlayer );
+ break;
+ }
+ }
+ }
+}
+
+CON_COMMAND( coaching_force_student, "Force self as student" )
+{
+ CTFPlayer* pStudentPlayer = ToTFPlayer( UTIL_GetListenServerHost() );
+ if ( pStudentPlayer && TFGameRules() )
+ {
+ for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ )
+ {
+ CTFPlayer *pCoachPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( pCoachPlayer != pStudentPlayer )
+ {
+ Coaching_Start( pCoachPlayer, pStudentPlayer );
+ break;
+ }
+ }
+ }
+}
+#endif
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::OnCoachJoining( uint32 unCoachAccountID, uint32 unStudentAccountID )
+{
+ m_mapCoachToStudentMap.Insert( unCoachAccountID, unStudentAccountID );
+ // see if the coach is on the server already
+ CSteamID steamIDForCoach;
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CTFPlayer *pPotentialCoach = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( NULL == pPotentialCoach )
+ {
+ continue;
+ }
+ // coach is here, force them to respawn, which will set them up as a coach
+ if ( pPotentialCoach->GetSteamID( &steamIDForCoach ) && steamIDForCoach.GetAccountID() == unCoachAccountID )
+ {
+ pPotentialCoach->ForceRespawn();
+ return;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::OnRemoveCoach( uint32 unCoachAccountID )
+{
+ m_mapCoachToStudentMap.Remove( unCoachAccountID );
+ for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ CSteamID steamID;
+ if ( pPlayer && pPlayer->GetSteamID( &steamID ) && steamID.GetAccountID() == unCoachAccountID )
+ {
+ Coaching_Stop( pPlayer );
+ return;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Activates 100% crits for an entire team for a short period of time
+//-----------------------------------------------------------------------------
+void CTFGameRules::HandleCTFCaptureBonus( int nTeam )
+{
+ float flBonusTime = GetCTFCaptureBonusTime();
+
+ if ( flBonusTime <= 0 )
+ return;
+
+ for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() == nTeam )
+ {
+ pPlayer->m_Shared.AddCond( TF_COND_CRITBOOSTED_CTF_CAPTURE, flBonusTime );
+ }
+ }
+}
+#endif
+
+int CTFGameRules::GetStatsMinimumPlayers( void )
+{
+ if ( IsInArenaMode() == true )
+ {
+ return 1;
+ }
+
+ return 3;
+}
+
+int CTFGameRules::GetStatsMinimumPlayedTime( void )
+{
+ if ( IsInArenaMode() == true )
+ {
+ return tf_arena_preround_time.GetFloat();
+ }
+
+ return 4 * 60; //Default of 4 minutes
+}
+
+bool CTFGameRules::IsConnectedUserInfoChangeAllowed( CBasePlayer *pPlayer )
+{
+ CTFPlayer* pTFPlayer = NULL;
+#ifdef GAME_DLL
+ pTFPlayer = ToTFPlayer( pPlayer );
+#else
+ pTFPlayer = ToTFPlayer( C_BasePlayer::GetLocalPlayer() );
+#endif
+
+ if( pTFPlayer )
+ {
+ // We can change if we're not alive
+ if( pTFPlayer->m_lifeState != LIFE_ALIVE )
+ return true;
+
+ // We can change if we're not on team red or blue
+ int iPlayerTeam = pTFPlayer->GetTeamNumber();
+ if( ( iPlayerTeam != TF_TEAM_RED ) && ( iPlayerTeam != TF_TEAM_BLUE ) )
+ return true;
+
+ // We can change if we've respawned/changed classes within the last 2 seconds.
+ // This allows for <classname>.cfg files to change these types of convars
+ float flRespawnTime = 0.f;
+#ifdef GAME_DLL
+ flRespawnTime = pTFPlayer->GetSpawnTime(); // Called everytime the player respawns
+#else
+ flRespawnTime = pTFPlayer->GetClassChangeTime(); // Called when the player changes class and respawns
+#endif
+ if( ( gpGlobals->curtime - flRespawnTime ) < 2.f )
+ return true;
+ }
+
+ return false;
+}
+
+//========================================================================================================================
+// BONUS ROUND HANDLING
+//========================================================================================================================
+#ifdef GAME_DLL
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::ShouldGoToBonusRound( void )
+{
+ // Only do this on a Valve official server. Use the presence of our DLL as a key.
+ return false;
+
+ if ( IsInTournamentMode() )
+ return false;
+
+ // Don't do this on empty servers
+ if ( !BHavePlayers() )
+ return false;
+ if ( TFTeamMgr()->GetTeam( TF_TEAM_RED )->GetNumPlayers() <= 0 )
+ return false;
+ if ( TFTeamMgr()->GetTeam( TF_TEAM_BLUE )->GetNumPlayers() <= 0 )
+ return false;
+
+ // Random chance per round, based on time.
+ float flRoundTime = gpGlobals->curtime - m_flRoundStartTime;
+ float flChance = RemapValClamped( flRoundTime, (3 * 60), (30 * 60), 0.0, 0.75 ); // 75% chance for > 30 min rounds, down to 0% at 3 min rounds
+ float flRoll = RandomFloat( 0, 1 );
+ return ( flRoll < flChance );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetupOnBonusStart( void )
+{
+ m_hBonusLogic.Set( dynamic_cast<CBonusRoundLogic*>(CreateEntityByName( "tf_logic_bonusround" )) );
+ if ( !m_hBonusLogic.Get()->InitBonusRound() )
+ {
+ State_Transition( GR_STATE_PREROUND );
+ return;
+ }
+
+ // Bring up the giveaway panel on all the players
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
+ if ( !pPlayer )
+ continue;
+
+ pPlayer->ShowViewPortPanel( PANEL_GIVEAWAY_ITEM );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetupOnBonusEnd( void )
+{
+ if ( m_hBonusLogic.Get() )
+ {
+ UTIL_Remove( m_hBonusLogic.Get() );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::BonusStateThink( void )
+{
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: We need to abort the bonus state because our item generation failed.
+// Steam is probably down.
+//-----------------------------------------------------------------------------
+void CTFGameRules::BonusStateAbort( void )
+{
+ if ( m_hBonusLogic.Get() )
+ {
+ m_hBonusLogic.Get()->SetBonusStateAborted( true );
+ }
+
+ State_Transition( GR_STATE_PREROUND );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::BetweenRounds_Start( void )
+{
+ SetSetup( true );
+
+ if ( IsMannVsMachineMode() )
+ {
+ mp_tournament.SetValue( true );
+ RestartTournament();
+ SetInStopWatch( false );
+
+ char szName[16];
+ Q_strncpy( szName, "ROBOTS", MAX_TEAMNAME_STRING + 1 );
+ mp_tournament_blueteamname.SetValue( szName );
+ Q_strncpy( szName, "MANNCO", MAX_TEAMNAME_STRING + 1 );
+ mp_tournament_redteamname.SetValue( szName );
+ SetTeamReadyState( true, TF_TEAM_PVE_INVADERS );
+ }
+
+ for ( int i = 0; i < IBaseObjectAutoList::AutoList().Count(); ++i )
+ {
+ CBaseObject *pObj = static_cast<CBaseObject*>( IBaseObjectAutoList::AutoList()[i] );
+ if ( pObj->IsDisposableBuilding() || pObj->GetTeamNumber() == TF_TEAM_PVE_INVADERS )
+ {
+ pObj->DetonateObject();
+ }
+ }
+
+ if ( m_hGamerulesProxy )
+ {
+ m_hGamerulesProxy->StateEnterBetweenRounds();
+ }
+
+ if ( m_hCompetitiveLogicEntity )
+ {
+ m_hCompetitiveLogicEntity->OnSpawnRoomDoorsShouldUnlock();
+ }
+
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc && pMatchDesc->m_params.m_bAutoReady )
+ {
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CTFPlayer *pPlayer = static_cast<CTFPlayer*>( UTIL_PlayerByIndex( i ) );
+ if ( !pPlayer )
+ continue;
+
+ if ( IsValidTFTeam( pPlayer->GetTeamNumber() ) && pPlayer->GetPlayerClass() && IsValidTFPlayerClass( pPlayer->GetPlayerClass()->GetClassIndex() ) )
+ {
+ PlayerReadyStatus_UpdatePlayerState( pPlayer, true );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::BetweenRounds_End( void )
+{
+ SetInWaitingForPlayers( false );
+ SetSetup( false );
+
+ if ( IsMannVsMachineMode() )
+ {
+ SetInStopWatch( false );
+ mp_tournament_stopwatch.SetValue( false );
+ }
+
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( !pPlayer )
+ continue;
+
+ // We don't consider inactivity during BetweenRounds as idle
+ pPlayer->ResetIdleCheck();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::BetweenRounds_Think( void )
+{
+ if ( UsePlayerReadyStatusMode() )
+ {
+ // Everyone is ready, or the drop-dead timer naturally ticked down to mp_tournament_readymode_countdown
+ bool bStartFinalCountdown = ( PlayerReadyStatus_ShouldStartCountdown() || ( m_flRestartRoundTime > 0 && (int)( m_flRestartRoundTime - gpGlobals->curtime ) == mp_tournament_readymode_countdown.GetInt() ) );
+
+ // It's the FINAL COUNTDOOOWWWNNnnnnnnnnn
+ float flDropDeadTime = gpGlobals->curtime + mp_tournament_readymode_countdown.GetFloat() + 0.1f;
+ if ( bStartFinalCountdown && ( m_flRestartRoundTime < 0 || m_flRestartRoundTime >= flDropDeadTime ) )
+ {
+ float flDelay = IsMannVsMachineMode() ? 10.f : mp_tournament_readymode_countdown.GetFloat();
+ m_flRestartRoundTime.Set( gpGlobals->curtime + flDelay );
+ ShouldResetScores( true, true );
+ ShouldResetRoundsPlayed( true );
+
+ if ( IsCompetitiveMode() )
+ {
+ m_flCompModeRespawnPlayersAtMatchStart = gpGlobals->curtime + 2.0;
+ }
+ }
+
+ // Required for UI state
+ if ( PlayerReadyStatus_HaveMinPlayersToEnable() )
+ {
+ CheckReadyRestart();
+ }
+ }
+
+ CheckRespawnWaves();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::PreRound_Start( void )
+{
+ if ( m_hGamerulesProxy )
+ {
+ m_hGamerulesProxy->StateEnterPreRound();
+ }
+
+ if ( m_hCompetitiveLogicEntity )
+ {
+ m_hCompetitiveLogicEntity->OnSpawnRoomDoorsShouldLock();
+ }
+
+ BaseClass::PreRound_Start();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::PreRound_End( void )
+{
+ if ( IsHalloweenScenario( HALLOWEEN_SCENARIO_HIGHTOWER ) && !IsInWaitingForPlayers() )
+ {
+ if ( RandomFloat( 0, 1 ) < HELLTOWER_RARE_LINE_CHANCE )
+ {
+ PlayHelltowerAnnouncerVO( HELLTOWER_VO_RED_ROUNDSTART_RARE, HELLTOWER_VO_BLUE_ROUNDSTART_RARE );
+ }
+ else
+ {
+ PlayHelltowerAnnouncerVO( HELLTOWER_VO_RED_ROUNDSTART, HELLTOWER_VO_BLUE_ROUNDSTART );
+ }
+ }
+
+ if ( m_hGamerulesProxy )
+ {
+ m_hGamerulesProxy->StateExitPreRound();
+ }
+
+ if ( m_hCompetitiveLogicEntity )
+ {
+ m_hCompetitiveLogicEntity->OnSpawnRoomDoorsShouldUnlock();
+ }
+
+ BaseClass::PreRound_End();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Compute internal vectors of health and ammo locations
+//-----------------------------------------------------------------------------
+void CTFGameRules::ComputeHealthAndAmmoVectors( void )
+{
+ m_ammoVector.RemoveAll();
+ m_healthVector.RemoveAll();
+
+ CBaseEntity *pEnt = gEntList.FirstEnt();
+ while( pEnt )
+ {
+ if ( pEnt->ClassMatches( "func_regenerate" ) || pEnt->ClassMatches( "item_healthkit*" ) )
+ {
+ m_healthVector.AddToTail( pEnt );
+ }
+
+ if ( pEnt->ClassMatches( "func_regenerate" ) || pEnt->ClassMatches( "item_ammopack*" ) )
+ {
+ m_ammoVector.AddToTail( pEnt );
+ }
+
+ pEnt = gEntList.NextEnt( pEnt );
+ }
+
+ m_areHealthAndAmmoVectorsReady = true;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Return vector of health entities
+//-----------------------------------------------------------------------------
+const CUtlVector< CHandle< CBaseEntity > > &CTFGameRules::GetHealthEntityVector( void )
+{
+ // lazy-populate health and ammo vector since some maps (Dario!) move these entities around between stages
+ if ( !m_areHealthAndAmmoVectorsReady )
+ {
+ ComputeHealthAndAmmoVectors();
+ }
+
+ return m_healthVector;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Return vector of ammo entities
+//-----------------------------------------------------------------------------
+const CUtlVector< CHandle< CBaseEntity > > &CTFGameRules::GetAmmoEntityVector( void )
+{
+ // lazy-populate health and ammo vector since some maps (Dario!) move these entities around between stages
+ if ( !m_areHealthAndAmmoVectorsReady )
+ {
+ ComputeHealthAndAmmoVectors();
+ }
+
+ return m_ammoVector;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Return the Payload cart the given team needs to push to win, or NULL if none currently exists
+//-----------------------------------------------------------------------------
+CHandle< CTeamTrainWatcher > CTFGameRules::GetPayloadToPush( int pushingTeam ) const
+{
+ if ( TFGameRules()->GetGameType() != TF_GAMETYPE_ESCORT )
+ return NULL;
+
+ if ( pushingTeam == TF_TEAM_RED )
+ {
+ if ( m_redPayloadToPush == NULL )
+ {
+ // find our cart!
+ if ( TFGameRules()->HasMultipleTrains() )
+ {
+ // find the red cart
+ }
+ else
+ {
+ // normal Escort scenario, red always blocks
+ return NULL;
+ }
+ }
+
+ return m_redPayloadToPush;
+ }
+
+ if ( pushingTeam == TF_TEAM_BLUE )
+ {
+ if ( m_bluePayloadToPush == NULL )
+ {
+ if ( TFGameRules()->HasMultipleTrains() )
+ {
+ // find the blue cart
+ }
+ else
+ {
+ // only one cart in the map, and we need to push it
+ CTeamTrainWatcher *watcher = NULL;
+ while( ( watcher = dynamic_cast< CTeamTrainWatcher * >( gEntList.FindEntityByClassname( watcher, "team_train_watcher" ) ) ) != NULL )
+ {
+ if ( !watcher->IsDisabled() )
+ {
+ m_bluePayloadToPush = watcher;
+ break;
+ }
+ }
+ }
+ }
+
+ return m_bluePayloadToPush;
+ }
+
+ return NULL;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Return the Payload cart the given player needs to block from advancing, or NULL if none currently exists
+//-----------------------------------------------------------------------------
+CHandle< CTeamTrainWatcher > CTFGameRules::GetPayloadToBlock( int blockingTeam ) const
+{
+ if ( TFGameRules()->GetGameType() != TF_GAMETYPE_ESCORT )
+ return NULL;
+
+ if ( blockingTeam == TF_TEAM_RED )
+ {
+ if ( m_redPayloadToBlock == NULL )
+ {
+ // find our cart!
+ if ( TFGameRules()->HasMultipleTrains() )
+ {
+ // find the red cart
+ }
+ else
+ {
+ // normal Escort scenario, red always blocks
+ CTeamTrainWatcher *watcher = NULL;
+ while( ( watcher = dynamic_cast< CTeamTrainWatcher * >( gEntList.FindEntityByClassname( watcher, "team_train_watcher" ) ) ) != NULL )
+ {
+ if ( !watcher->IsDisabled() )
+ {
+ m_redPayloadToBlock = watcher;
+ break;
+ }
+ }
+ }
+ }
+
+ return m_redPayloadToBlock;
+ }
+
+ if ( blockingTeam == TF_TEAM_BLUE )
+ {
+ if ( m_bluePayloadToBlock == NULL )
+ {
+ if ( TFGameRules()->HasMultipleTrains() )
+ {
+ // find the blue cart
+ }
+ else
+ {
+ // normal Payload, blue never blocks
+ return NULL;
+ }
+ }
+
+ return m_bluePayloadToBlock;
+ }
+
+ return NULL;
+}
+
+
+#endif // GAME_DLL
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::BuildBonusPlayerList( void )
+{
+ if ( m_hBonusLogic.Get() )
+ {
+ m_hBonusLogic.Get()->BuildBonusPlayerList();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Item testing bot controls
+//-----------------------------------------------------------------------------
+void CTFGameRules::ItemTesting_SetupFromKV( KeyValues *pKV )
+{
+ m_iItemTesting_BotAnim = pKV->GetInt( "bot_anim", TI_BOTANIM_IDLE );
+
+ int iAnimSpeed = pKV->GetInt( "bot_animspeed", 100 );
+ m_flItemTesting_BotAnimSpeed = ( (float)iAnimSpeed / 100.0f );
+
+ m_bItemTesting_BotForceFire = pKV->GetBool( "bot_force_fire" );
+ m_bItemTesting_BotTurntable = pKV->GetBool( "bot_turntable" );
+ m_bItemTesting_BotViewScan = pKV->GetBool( "bot_view_scan" );
+}
+
+//==================================================================================================================
+// BONUS ROUND LOGIC
+#ifndef CLIENT_DLL
+EXTERN_SEND_TABLE( DT_ScriptCreatedItem );
+#else
+EXTERN_RECV_TABLE( DT_ScriptCreatedItem );
+#endif
+
+BEGIN_NETWORK_TABLE_NOBASE( CBonusRoundLogic, DT_BonusRoundLogic )
+#ifdef CLIENT_DLL
+ RecvPropUtlVector( RECVINFO_UTLVECTOR( m_aBonusPlayerRoll ), MAX_PLAYERS, RecvPropInt( NULL, 0, 4 ) ),
+ RecvPropEHandle( RECVINFO( m_hBonusWinner ) ),
+ RecvPropDataTable(RECVINFO_DT(m_Item), 0, &REFERENCE_RECV_TABLE(DT_ScriptCreatedItem)),
+#else
+ SendPropUtlVector( SENDINFO_UTLVECTOR( m_aBonusPlayerRoll ), MAX_PLAYERS, SendPropInt( NULL, 0, 4, 12, SPROP_UNSIGNED ) ),
+ SendPropEHandle( SENDINFO( m_hBonusWinner ) ),
+ SendPropDataTable(SENDINFO_DT(m_Item), &REFERENCE_SEND_TABLE(DT_ScriptCreatedItem)),
+#endif
+END_NETWORK_TABLE()
+
+LINK_ENTITY_TO_CLASS( tf_logic_bonusround, CBonusRoundLogic );
+IMPLEMENT_NETWORKCLASS_ALIASED( BonusRoundLogic, DT_BonusRoundLogic )
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CBonusRoundLogic::BuildBonusPlayerList( void )
+{
+ m_aBonusPlayerList.Purge();
+
+ for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+ if ( !pPlayer )
+ continue;
+
+ m_aBonusPlayerList.InsertNoSort( pPlayer );
+ }
+
+ m_aBonusPlayerList.RedoSort( true );
+}
+
+#ifdef GAME_DLL
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CBonusRoundLogic::InitBonusRound( void )
+{
+ m_bAbortedBonusRound = false;
+ SetBonusItem( 0 );
+ m_hBonusWinner = NULL;
+
+ BuildBonusPlayerList();
+
+ // We actually precalculate the whole shebang.
+ m_aBonusPlayerRoll.SetSize( m_aBonusPlayerList.Count() );
+ for ( int i = 0; i < m_aBonusPlayerRoll.Count(); i++ )
+ {
+ m_aBonusPlayerRoll[i] = RandomInt( PLAYER_ROLL_MIN, PLAYER_ROLL_MAX );
+ }
+
+ // Sum up the bonus chances
+ int iTotal = 0;
+ CUtlVector<int> aBonusPlayerTotals;
+ aBonusPlayerTotals.SetSize( m_aBonusPlayerList.Count() );
+ for ( int i = 0; i < aBonusPlayerTotals.Count(); i++ )
+ {
+ aBonusPlayerTotals[i] = iTotal + m_aBonusPlayerRoll[i] + m_aBonusPlayerList[i]->m_Shared.GetItemFindBonus();
+ iTotal = aBonusPlayerTotals[i];
+ }
+
+ // Roll for who gets the item.
+ int iRoll = RandomInt( 0, iTotal );
+ for ( int i = 0; i < aBonusPlayerTotals.Count(); i++ )
+ {
+ if ( iRoll < aBonusPlayerTotals[i] )
+ {
+ m_hBonusWinner = m_aBonusPlayerList[i];
+ break;
+ }
+ }
+
+ if ( !m_hBonusWinner.Get() )
+ return false;
+
+ // Generate the item on the server for now
+ //CSteamID steamID;
+ //if ( !m_hBonusWinner.Get()->GetSteamID( &steamID ) )
+ const CSteamID *steamID = engine->GetGameServerSteamID();
+ if ( !steamID || !steamID->IsValid() )
+ return false;
+
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CBonusRoundLogic::SetBonusItem( itemid_t iItemID )
+{
+ m_iBonusItemID = iItemID;
+ if ( !m_iBonusItemID )
+ {
+ m_Item.Invalidate();
+ return;
+ }
+
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetBonusItem( itemid_t iItemID )
+{
+ if ( m_hBonusLogic.Get() )
+ {
+ m_hBonusLogic.Get()->SetBonusItem( iItemID );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::ProcessVerboseLogOutput( void )
+{
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
+
+ if ( pPlayer && ( pPlayer->GetTeamNumber() > TEAM_UNASSIGNED ) )
+ {
+ UTIL_LogPrintf( "\"%s<%i><%s><%s>\" position_report (position \"%d %d %d\")\n",
+ pPlayer->GetPlayerName(),
+ pPlayer->GetUserID(),
+ pPlayer->GetNetworkIDString(),
+ pPlayer->GetTeam()->GetName(),
+ (int)pPlayer->GetAbsOrigin().x,
+ (int)pPlayer->GetAbsOrigin().y,
+ (int)pPlayer->GetAbsOrigin().z );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::MatchSummaryTeleport()
+{
+ bool bUseMatchSummaryStage = false;
+#ifdef STAGING_ONLY
+ bUseMatchSummaryStage = tf_test_match_summary.GetBool();
+#endif
+
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( GetCurrentMatchGroup() );
+ if ( pMatchDesc && pMatchDesc->m_params.m_bUseMatchSummaryStage )
+ {
+ bUseMatchSummaryStage = true;
+ }
+
+ if ( bUseMatchSummaryStage && m_bMapHasMatchSummaryStage )
+ {
+ RespawnPlayers( true );
+
+ // find the observer target for the stage
+ CObserverPoint *pObserverPoint = dynamic_cast<CObserverPoint*>( gEntList.FindEntityByClassname( NULL, "info_observer_point" ) );
+ while( pObserverPoint )
+ {
+ if ( pObserverPoint->IsMatchSummary() )
+ {
+ pObserverPoint->SetDisabled( false );
+ SetRequiredObserverTarget( pObserverPoint );
+ break;
+ }
+
+ pObserverPoint = dynamic_cast<CObserverPoint*>( gEntList.FindEntityByClassname( pObserverPoint, "info_observer_point" ) );
+ }
+
+ // need to do this AFTER we respawn the players above or the conditions will be cleared
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
+ if ( !pPlayer )
+ continue;
+
+ pPlayer->AddFlag( FL_FROZEN );
+
+ if ( pPlayer->GetTeamNumber() >= FIRST_GAME_TEAM ) // spectators automatically get the RequiredObserverTarget that was set above
+ {
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+ if ( pTFPlayer )
+ {
+ pTFPlayer->m_Shared.AddCond( ( pTFPlayer->GetTeamNumber() == GetWinningTeam() ) ? TF_COND_COMPETITIVE_WINNER : TF_COND_COMPETITIVE_LOSER );
+
+ if ( pObserverPoint )
+ {
+ pTFPlayer->SetViewEntity( pObserverPoint );
+ pTFPlayer->SetViewOffset( vec3_origin );
+ pTFPlayer->SetFOV( pObserverPoint, pObserverPoint->m_flFOV );
+ }
+
+ // use this to force the client player anim to face the right direction
+ pTFPlayer->SetTauntYaw( pTFPlayer->GetAbsAngles()[YAW] );
+ }
+ }
+ }
+
+ m_bPlayersAreOnMatchSummaryStage.Set( true );
+ }
+}
+
+#ifdef STAGING_ONLY
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::MatchSummaryTest( void )
+{
+ mp_waitingforplayers_cancel.SetValue( 1 );
+ tf_test_match_summary.SetValue( 1 );
+ g_fGameOver = true;
+ TFGameRules()->State_Transition( GR_STATE_GAME_OVER );
+ m_flStateTransitionTime = gpGlobals->curtime + 99999.f;
+ TFGameRules()->MatchSummaryStart();
+}
+
+CON_COMMAND ( show_match_summary, "Show the match summary" )
+{
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ if ( args.ArgC() < 2 )
+ return;
+
+ if ( FStrEq( args[1], "start" ) )
+ {
+ TFGameRules()->MatchSummaryTest();
+ }
+ else if ( FStrEq( args[1], "end" ) )
+ {
+ TFGameRules()->MatchSummaryEnd();
+ }
+}
+
+#endif // STAGING_ONLY
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::MatchSummaryStart( void )
+{
+ if ( BAttemptMapVoteRollingMatch() )
+ {
+ // Grab the final list of maps for users to vote on
+ UpdateNextMapVoteOptionsFromLobby();
+ m_eRematchState = NEXT_MAP_VOTE_STATE_WAITING_FOR_USERS_TO_VOTE;
+ }
+
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
+ if ( pPlayer )
+ {
+ pPlayer->AddFlag( FL_FROZEN );
+ }
+ }
+
+ m_bShowMatchSummary.Set( true );
+ m_flMatchSummaryTeleportTime = gpGlobals->curtime + 2.f;
+
+ if ( m_hGamerulesProxy )
+ {
+ m_hGamerulesProxy->MatchSummaryStart();
+ }
+
+ CBaseEntity *pLogicCase = NULL;
+ while ( ( pLogicCase = gEntList.FindEntityByName( pLogicCase, "competitive_stage_logic_case" ) ) != NULL )
+ {
+ if ( pLogicCase )
+ {
+ variant_t sVariant;
+ sVariant.SetInt( GetWinningTeam() );
+ pLogicCase->AcceptInput( "InValue", NULL, NULL, sVariant, 0 );
+ break;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::MatchSummaryEnd( void )
+{
+ m_bShowMatchSummary.Set( false );
+ m_bPlayersAreOnMatchSummaryStage.Set( false );
+
+ SetRequiredObserverTarget( NULL );
+
+ for ( int i = 1; i <= MAX_PLAYERS; i++ )
+ {
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
+ if ( !pPlayer )
+ continue;
+
+ pPlayer->RemoveFlag( FL_FROZEN );
+ pPlayer->SetViewEntity( NULL );
+ pPlayer->SetFOV( pPlayer, 0 );
+ }
+
+ // reset bot convars here
+ static ConVarRef tf_bot_quota( "tf_bot_quota" );
+ tf_bot_quota.SetValue( tf_bot_quota.GetDefault() );
+ static ConVarRef tf_bot_quota_mode( "tf_bot_quota_mode" );
+ tf_bot_quota_mode.SetValue( tf_bot_quota_mode.GetDefault() );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+int CTFGameRules::GetTeamAssignmentOverride( CTFPlayer *pTFPlayer, int iDesiredTeam, bool bAutoBalance /*= false*/ )
+{
+ int iTeam = iDesiredTeam;
+
+ // Look up GC managed match info
+ CSteamID steamID;
+ pTFPlayer->GetSteamID( &steamID );
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetLiveMatch();
+ int nMatchPlayers = pMatch ? pMatch->GetNumActiveMatchPlayers() : 0;
+ CMatchInfo::PlayerMatchData_t *pMatchPlayer = ( pMatch && steamID.IsValid() ) ? pMatch->GetMatchDataForPlayer( steamID ) : NULL;
+
+ if ( IsMannVsMachineMode() )
+ {
+ if ( !pTFPlayer->IsBot() && iTeam != TEAM_SPECTATOR )
+ {
+ if ( pMatchPlayer && !pMatchPlayer->bDropped )
+ {
+ // Part of the lobby match
+ Log( "MVM assigned %s to defending team (player is in lobby)\n", pTFPlayer->GetPlayerName() );
+ return TF_TEAM_PVE_DEFENDERS;
+ }
+
+ // Count ad-hoc players on defenders team
+ int nAdHocDefenders = 0;
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
+
+ if ( !pPlayer || ( pPlayer->GetTeamNumber() != TF_TEAM_PVE_DEFENDERS ) )
+ { continue; }
+
+ CSteamID steamID;
+ if ( pPlayer->GetSteamID( &steamID ) && GTFGCClientSystem()->GetLiveMatchPlayer( steamID ) )
+ { continue; }
+
+ // Player on defenders that doesn't have a live match entry
+ nAdHocDefenders++;
+ }
+
+ // Bootcamp mode can mix a lobby with ad-hoc joins
+ int nSlotsLeft = kMVM_DefendersTeamSize - nMatchPlayers - nAdHocDefenders;
+ if ( nSlotsLeft >= 1 )
+ {
+ Log( "MVM assigned %s to defending team (%d more slots remaining after us)\n", pTFPlayer->GetPlayerName(), nSlotsLeft-1 );
+
+ // Set Their Currency
+ int nRoundCurrency = MannVsMachineStats_GetAcquiredCredits();
+ nRoundCurrency += g_pPopulationManager->GetStartingCurrency();
+
+ // deduct any cash that has already been spent
+ int spentCurrency = g_pPopulationManager->GetPlayerCurrencySpent( pTFPlayer );
+ pTFPlayer->SetCurrency( nRoundCurrency - spentCurrency );
+
+ iTeam = TF_TEAM_PVE_DEFENDERS;
+ }
+ else
+ {
+ // no room
+ Log( "MVM assigned %s to spectator, all slots for defending team are in use, or reserved for lobby members\n",
+ pTFPlayer->GetPlayerName() );
+ iTeam = TEAM_SPECTATOR;
+ }
+ }
+ }
+ else if ( pMatch )
+ {
+ if ( !bAutoBalance )
+ {
+ CSteamID steamID;
+ if ( pMatchPlayer )
+ {
+ iTeam = GetGameTeamForGCTeam( pMatchPlayer->eGCTeam );
+ if ( iTeam < FIRST_GAME_TEAM )
+ {
+ // We should always have a team assigned by the GC
+ Warning( "Competitive mode: Lobby player with invalid GC team %i in MatchGroup %i\n", iTeam, (int)pMatch->m_eMatchGroup );
+ }
+ CheckAndSetPartyLeader( pTFPlayer, iTeam );
+ }
+ }
+ }
+
+ return iTeam;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+static CPhysicsProp *CreatePhysicsToy( const char* pszModelName, const Vector &vSpawnPos, const QAngle &qSpawnAngles )
+{
+ if ( pszModelName == NULL )
+ return NULL;
+
+ CPhysicsProp *pProp = NULL;
+
+ MDLCACHE_CRITICAL_SECTION();
+
+ MDLHandle_t h = mdlcache->FindMDL( pszModelName );
+ if ( h != MDLHANDLE_INVALID )
+ {
+ // Must have vphysics to place as a physics prop
+ studiohdr_t *pStudioHdr = mdlcache->GetStudioHdr( h );
+ if ( pStudioHdr && mdlcache->GetVCollide( h ) )
+ {
+ bool bAllowPrecache = CBaseEntity::IsPrecacheAllowed();
+ CBaseEntity::SetAllowPrecache( true );
+
+ // Try to create entity
+ pProp = dynamic_cast< CPhysicsProp * >( CreateEntityByName( "prop_physics_override" ) );
+ if ( pProp )
+ {
+ pProp->SetCollisionGroup( COLLISION_GROUP_PUSHAWAY );
+ // so it can be pushed by airblast
+ pProp->AddFlag( FL_GRENADE );
+ // so that it will always be interactable with the player
+ //pProp->SetPhysicsMode( PHYSICS_MULTIPLAYER_SOLID );//PHYSICS_MULTIPLAYER_NON_SOLID );
+ char buf[512];
+ // Pass in standard key values
+ Q_snprintf( buf, sizeof(buf), "%.10f %.10f %.10f", vSpawnPos.x, vSpawnPos.y, vSpawnPos.z );
+ pProp->KeyValue( "origin", buf );
+ Q_snprintf( buf, sizeof(buf), "%.10f %.10f %.10f", qSpawnAngles.x, qSpawnAngles.y, qSpawnAngles.z );
+ pProp->KeyValue( "angles", buf );
+ pProp->KeyValue( "model", pszModelName );
+ pProp->KeyValue( "fademindist", "-1" );
+ pProp->KeyValue( "fademaxdist", "0" );
+ pProp->KeyValue( "fadescale", "1" );
+ pProp->KeyValue( "inertiaScale", "1.0" );
+ pProp->KeyValue( "physdamagescale", "0.1" );
+ pProp->Precache();
+ DispatchSpawn( pProp );
+ pProp->m_takedamage = DAMAGE_YES; // Take damage, otherwise this can block trains
+ pProp->SetHealth( 5000 );
+ pProp->Activate();
+ }
+
+ CBaseEntity::SetAllowPrecache( bAllowPrecache );
+ }
+
+ mdlcache->Release( h ); // counterbalance addref from within FindMDL
+ }
+ return pProp;
+}
+
+CPhysicsProp *CreateBeachBall( const Vector &vSpawnPos, const QAngle &qSpawnAngles )
+{
+ return CreatePhysicsToy( "models/props_gameplay/ball001.mdl", vSpawnPos, qSpawnAngles );
+}
+
+CPhysicsProp *CreateSoccerBall( const Vector &vSpawnPos, const QAngle &qSpawnAngles )
+{
+ return CreatePhysicsToy( "models/player/items/scout/soccer_ball.mdl", vSpawnPos, qSpawnAngles );
+}
+
+
+void CTFGameRules::PushAllPlayersAway( const Vector& vFromThisPoint, float flRange, float flForce, int nTeam, CUtlVector< CTFPlayer* > *pPushedPlayers /*= NULL*/ )
+{
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, nTeam, COLLECT_ONLY_LIVING_PLAYERS );
+
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ CTFPlayer *pPlayer = playerVector[i];
+
+ Vector toPlayer = pPlayer->EyePosition() - vFromThisPoint;
+
+ if ( toPlayer.LengthSqr() < flRange * flRange )
+ {
+ // send the player flying
+ // make sure we push players up and away
+ toPlayer.z = 0.0f;
+ toPlayer.NormalizeInPlace();
+ toPlayer.z = 1.0f;
+
+ Vector vPush = flForce * toPlayer;
+
+ pPlayer->ApplyAbsVelocityImpulse( vPush );
+
+ if ( pPushedPlayers )
+ {
+ pPushedPlayers->AddToTail( pPlayer );
+ }
+ }
+ }
+}
+
+#endif // GAME_DLL
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameRules::CanUpgradeWithAttrib( CTFPlayer *pPlayer, int iWeaponSlot, attrib_definition_index_t iAttribIndex, CMannVsMachineUpgrades *pUpgrade )
+{
+ if ( !pPlayer )
+ return false;
+
+ Assert ( pUpgrade );
+
+ // Upgrades on players are considered active at all times
+ if ( pUpgrade->nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_PLAYER )
+ {
+ switch ( iAttribIndex )
+ {
+ case 113: // "metal regen"
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_ENGINEER ) );
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ // Get the item entity. We use the entity, not the item in the loadout, because we want
+ // the dynamic attributes that have already been purchases and attached.
+ CEconEntity *pEntity;
+ CEconItemView *pCurItemData = CTFPlayerSharedUtils::GetEconItemViewByLoadoutSlot( pPlayer, iWeaponSlot, &pEntity );
+ if ( !pCurItemData || !pEntity )
+ return false;
+
+ // bottles can only hold things in the appropriate ui group
+ if ( dynamic_cast< CTFPowerupBottle *>( pEntity ) )
+ {
+ if ( pUpgrade->nUIGroup == UIGROUP_POWERUPBOTTLE )
+ {
+ switch ( iAttribIndex )
+ {
+ case 327: // "building instant upgrade"
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_ENGINEER ) );
+ }
+#ifndef _DEBUG
+ case 480: // "radius stealth"
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_SPY ) );
+ }
+#endif // !_DEBUG
+ }
+ return true;
+ }
+
+ return false;
+ }
+ else if ( pUpgrade->nUIGroup == UIGROUP_POWERUPBOTTLE )
+ {
+ return false;
+ }
+
+ CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase* > ( pEntity );
+ CTFWeaponBaseGun *pWeaponGun = dynamic_cast< CTFWeaponBaseGun* > ( pEntity );
+ int iWeaponID = ( pWeapon ) ? pWeapon->GetWeaponID() : TF_WEAPON_NONE;
+ CTFWearableDemoShield *pShield = dynamic_cast< CTFWearableDemoShield* >( pEntity );
+ bool bShield = ( pShield ) ? true : false;
+
+ if ( iWeaponID == TF_WEAPON_PARACHUTE )
+ return false;
+
+ // Hack to simplify excluding non-weapons from damage upgrades
+ bool bHideDmgUpgrades = iWeaponID == TF_WEAPON_NONE ||
+ iWeaponID == TF_WEAPON_LASER_POINTER ||
+ iWeaponID == TF_WEAPON_MEDIGUN ||
+ iWeaponID == TF_WEAPON_BUFF_ITEM ||
+ iWeaponID == TF_WEAPON_BUILDER ||
+ iWeaponID == TF_WEAPON_PDA_ENGINEER_BUILD ||
+ iWeaponID == TF_WEAPON_INVIS ||
+ iWeaponID == TF_WEAPON_SPELLBOOK;
+
+ // What tier upgrade is it?
+ int nQuality = pUpgrade->nQuality;
+
+ // This is crappy, but it's hopefully more maintainable than the allowed attributes block for all current & future items
+ switch ( iAttribIndex )
+ {
+ case 2: // "damage bonus"
+ {
+ if ( bHideDmgUpgrades )
+ return false;
+
+ // Some classes get a weaker dmg upgrade
+ if ( nQuality == MVM_UPGRADE_QUALITY_LOW )
+ {
+ if ( pPlayer->IsPlayerClass( TF_CLASS_DEMOMAN ) && !bShield )
+ {
+ return ( iWeaponSlot == TF_WPN_TYPE_PRIMARY || iWeaponSlot == TF_WPN_TYPE_SECONDARY );
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ bool bShieldEquipped = false;
+ if ( pPlayer->IsPlayerClass( TF_CLASS_DEMOMAN ) )
+ {
+ for ( int i = 0; i < pPlayer->GetNumWearables(); ++i )
+ {
+ CTFWearableDemoShield *pWearableShield = dynamic_cast< CTFWearableDemoShield* >( pPlayer->GetWearable( i ) );
+ if ( pWearableShield )
+ {
+ bShieldEquipped = true;
+ }
+ }
+ }
+
+ return ( ( iWeaponSlot == TF_WPN_TYPE_PRIMARY &&
+ ( pPlayer->IsPlayerClass( TF_CLASS_SCOUT ) ||
+ pPlayer->IsPlayerClass( TF_CLASS_SNIPER ) ||
+ pPlayer->IsPlayerClass( TF_CLASS_SOLDIER ) ||
+ pPlayer->IsPlayerClass( TF_CLASS_PYRO ) ) ) ||
+ ( iWeaponID == TF_WEAPON_SWORD && bShieldEquipped ) );
+ }
+ break;
+ case 6: // "fire rate bonus"
+ {
+ // Heavy's version of firing speed costs more
+ bool bMinigun = iWeaponID == TF_WEAPON_MINIGUN;
+ if ( nQuality == MVM_UPGRADE_QUALITY_LOW )
+ {
+ return bMinigun;
+ }
+ else if ( iWeaponID == TF_WEAPON_GRENADELAUNCHER && pWeapon && pWeapon->AutoFiresFullClipAllAtOnce() )
+ {
+ return false;
+ }
+
+ // Non-melee version
+ return ( dynamic_cast< CTFWeaponBaseMelee* >( pEntity ) == NULL &&
+ iWeaponID != TF_WEAPON_NONE && !bHideDmgUpgrades &&
+ iWeaponID != TF_WEAPON_FLAMETHROWER &&
+ !WeaponID_IsSniperRifleOrBow( iWeaponID ) &&
+ !( pWeapon && pWeapon->HasEffectBarRegeneration() ) &&
+ !bMinigun );
+ }
+ break;
+ // case 8: // "heal rate bonus"
+ case 10: // "ubercharge rate bonus"
+ case 314: // "uber duration bonus"
+ case 481: // "canteen specialist"
+ case 482: // "overheal expert"
+ case 483: // "medic machinery beam"
+ case 493: // "healing mastery"
+ {
+ return ( iWeaponID == TF_WEAPON_MEDIGUN );
+ }
+ break;
+ case 31: // "critboost on kill"
+ {
+ CTFWeaponBaseMelee *pMelee = dynamic_cast<CTFWeaponBaseMelee *> ( pEntity );
+ return ( pMelee && (
+ pPlayer->IsPlayerClass( TF_CLASS_DEMOMAN ) ||
+ pPlayer->IsPlayerClass( TF_CLASS_SPY ) ) );
+ }
+ case 71: // weapon burn dmg increased
+ case 73: // weapon burn time increased
+ {
+ return ( iWeaponID == TF_WEAPON_FLAMETHROWER || iWeaponID == TF_WEAPON_FLAREGUN );
+ }
+ break;
+ case 76: // "maxammo primary increased"
+ {
+ return ( pWeapon && pWeapon->GetPrimaryAmmoType() == TF_AMMO_PRIMARY && !pWeapon->IsEnergyWeapon() );
+ }
+ break;
+ case 78: // "maxammo secondary increased"
+ {
+ return ( pWeapon && pWeapon->GetPrimaryAmmoType() == TF_AMMO_SECONDARY && !pWeapon->IsEnergyWeapon() );
+ }
+ break;
+ case 90: // "SRifle Charge rate increased"
+ {
+ return WeaponID_IsSniperRifle( iWeaponID );
+ }
+ break;
+ case 103: // "Projectile speed increased"
+ {
+ if ( pWeaponGun )
+ {
+ return ( pWeaponGun->GetWeaponProjectileType() == TF_PROJECTILE_PIPEBOMB );
+ }
+
+ return false;
+ }
+ break;
+ case 149: // "bleed duration"
+ case 523: // "arrow mastery"
+ {
+ return ( iWeaponID == TF_WEAPON_COMPOUND_BOW );
+ }
+ break;
+ case 218: // "mark for death"
+ {
+ return ( iWeaponID == TF_WEAPON_BAT_WOOD );
+ }
+ break;
+ case 249: // "charge recharge rate increased"
+ case 252: // "damage force reduction"
+ {
+ return bShield;
+ }
+ break;
+ case 255: // "airblast pushback scale"
+ {
+ return ( iWeaponID == TF_WEAPON_FLAMETHROWER &&
+ pWeaponGun && assert_cast< CTFFlameThrower* >( pWeaponGun )->SupportsAirBlastFunction( TF_FUNCTION_AIRBLAST_PUSHBACK) );
+ }
+ break;
+ case 266: // "projectile penetration"
+ {
+ if ( pWeaponGun && !bHideDmgUpgrades )
+ {
+ if ( !( pPlayer->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && iWeaponSlot == TF_WPN_TYPE_PRIMARY ) )
+ {
+ int iProjectile = pWeaponGun->GetWeaponProjectileType();
+ return ( iProjectile == TF_PROJECTILE_ARROW || iProjectile == TF_PROJECTILE_BULLET || iProjectile == TF_PROJECTILE_HEALING_BOLT || iProjectile == TF_PROJECTILE_FESTIVE_ARROW || iProjectile == TF_PROJECTILE_FESTIVE_HEALING_BOLT );
+ }
+ }
+
+ return false;
+ }
+ break;
+ case 80: // "maxammo metal increased"
+ case 276: // "bidirectional teleport"
+ case 286: // "engy building health bonus"
+ case 343: // "engy sentry fire rate increased"
+ case 345: // "engy dispenser radius increased"
+ case 351: // "engy disposable sentries"
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_ENGINEER ) && iWeaponID == TF_WEAPON_PDA_ENGINEER_BUILD );
+ }
+ break;
+ case 278: // "effect bar recharge rate increased"
+ {
+ return ( pWeapon && pWeapon->HasEffectBarRegeneration() && iWeaponID != TF_WEAPON_BUILDER && iWeaponID != TF_WEAPON_SPELLBOOK );
+ }
+ break;
+ case 279: // "maxammo grenades1 increased"
+ {
+ return ( iWeaponID == TF_WEAPON_BAT_WOOD || iWeaponID == TF_WEAPON_BAT_GIFTWRAP );
+ }
+ break;
+ case 313: // "applies snare effect"
+ {
+ return ( iWeaponID == TF_WEAPON_JAR || iWeaponID == TF_WEAPON_JAR_MILK );
+ }
+ break;
+ case 318: // "faster reload rate"
+ {
+ return ( ( pWeapon && pWeapon->ReloadsSingly() ) ||
+ WeaponID_IsSniperRifleOrBow( iWeaponID ) ||
+ iWeaponID == TF_WEAPON_FLAREGUN );
+ }
+ break;
+ case 319: // "increase buff duration"
+ {
+ return ( iWeaponID == TF_WEAPON_BUFF_ITEM );
+ }
+ break;
+ case 320: // "robo sapper"
+#ifdef STAGING_ONLY
+ case 601: // "ability spy traps"
+ case 603: // "phase cloak"
+#endif // STAGING_ONLY
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_SPY ) && iWeaponID == TF_WEAPON_BUILDER );
+ }
+ break;
+ case 323: // "attack projectiles"
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && iWeaponSlot == TF_WPN_TYPE_PRIMARY );
+ }
+ break;
+ case 335: // "clip size bonus upgrade"
+ {
+ return ( pWeapon && !pWeapon->IsBlastImpactWeapon() && pWeapon->UsesClipsForAmmo1() && pWeapon->GetMaxClip1() > 1
+ && iWeaponID != TF_WEAPON_FLAREGUN_REVENGE && iWeaponID != TF_WEAPON_SPELLBOOK );
+ }
+ break;
+ case 375: // "generate rage on damage"
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && iWeaponSlot == TF_WPN_TYPE_PRIMARY );
+ }
+ break;
+ case 395: // "explosive sniper shot"
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_SNIPER ) && iWeaponSlot == TF_WPN_TYPE_PRIMARY &&
+ pWeaponGun && pWeaponGun->GetWeaponProjectileType() == TF_PROJECTILE_BULLET );
+ }
+ break;
+ case 396: // "melee attack rate bonus"
+ {
+ // Melee version
+ return ( dynamic_cast< CTFWeaponBaseMelee* >( pEntity ) != NULL && iWeaponID != TF_WEAPON_BUFF_ITEM &&
+ !( pWeapon && pWeapon->HasEffectBarRegeneration() && iWeaponID != TF_WEAPON_BAT_WOOD ) );
+ }
+ break;
+ case 397: // "projectile penetration heavy"
+ {
+ if ( !bHideDmgUpgrades )
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && iWeaponSlot == TF_WPN_TYPE_PRIMARY );
+ }
+ }
+ break;
+ case 399: // "armor piercing"
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_SPY ) && iWeaponID == TF_WEAPON_KNIFE );
+ }
+ break;
+ case 440: // "clip size upgrade atomic"
+ {
+ return pWeapon && pWeapon->IsBlastImpactWeapon();
+ }
+ break;
+ case 484: // "mad milk syringes"
+ {
+ return ( iWeaponID == TF_WEAPON_SYRINGEGUN_MEDIC );
+ }
+ case 488: // "rocket specialist"
+ if ( pWeaponGun )
+ {
+ return ( pWeaponGun->GetWeaponProjectileType() == TF_PROJECTILE_ROCKET || pWeaponGun->GetWeaponProjectileType() == TF_PROJECTILE_ENERGY_BALL );
+ }
+ case 499: // generate rage on heal (shield)
+ case 554: // revive
+ case 555: // medigun specialist
+ {
+ return ( iWeaponID == TF_WEAPON_MEDIGUN );
+ }
+#ifdef STAGING_ONLY
+ case 553: // rocket pack
+ case 558: // mod flamethrower napalm
+ {
+ return ( iWeaponID == TF_WEAPON_FLAMETHROWER );
+ }
+ case 604: // sniper cloak
+ case 605: // master sniper
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_SNIPER ) && iWeaponSlot == TF_WPN_TYPE_PRIMARY );
+ }
+ case 611: // airborne infantry
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_SOLDIER ) && iWeaponSlot == TF_WPN_TYPE_PRIMARY );
+ }
+ case 624: // construction expert
+ case 626: // support engineer
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_ENGINEER ) && iWeaponSlot == TF_WPN_TYPE_MELEE );
+ }
+ case 631: // ability doubletap teleport
+ {
+ return ( pPlayer->IsPlayerClass( TF_CLASS_SCOUT ) && iWeaponSlot == TF_WPN_TYPE_PRIMARY );
+ }
+#endif // STAGING_ONLY
+ }
+
+ // All weapon related attributes require an item that does damage
+ if ( pUpgrade->nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_ITEM )
+ {
+ // All guns
+ if ( pWeaponGun )
+ return ( iWeaponID != TF_WEAPON_NONE && !bHideDmgUpgrades &&
+ !( pWeapon && pWeapon->HasEffectBarRegeneration() ) );
+
+ CTFWeaponBaseMelee *pMelee = dynamic_cast< CTFWeaponBaseMelee* >( pEntity );
+ if ( pMelee )
+ {
+ // All melee weapons except buff banners
+ return ( iWeaponID != TF_WEAPON_BUFF_ITEM && !bHideDmgUpgrades );
+ }
+
+ return false;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+//
+//-----------------------------------------------------------------------------
+int CTFGameRules::GetUpgradeTier( int iUpgrade )
+{
+ if ( !GameModeUsesUpgrades() || iUpgrade < 0 || iUpgrade >= g_MannVsMachineUpgrades.m_Upgrades.Count() )
+ return 0;
+
+ return g_MannVsMachineUpgrades.m_Upgrades[iUpgrade].nTier;
+}
+
+//-----------------------------------------------------------------------------
+// Given an upgrade and slot, see if its' tier is enabled/available
+//-----------------------------------------------------------------------------
+bool CTFGameRules::IsUpgradeTierEnabled( CTFPlayer *pTFPlayer, int iItemSlot, int iUpgrade )
+{
+ if ( !pTFPlayer )
+ return false;
+
+ // If the upgrade has a tier, it's mutually exclusive with upgrades of the same tier for the same slot
+ int nTier = GetUpgradeTier( iUpgrade );
+ if ( !nTier )
+ return false;
+
+ bool bIsAvailable = true;
+ CEconItemView *pItem = NULL;
+ float flValue = 0.f;
+
+ // Go through the upgrades and see if it could apply, and if we already have it
+ for ( int i = 0; i < g_MannVsMachineUpgrades.m_Upgrades.Count(); i++ )
+ {
+ CMannVsMachineUpgrades upgrade = g_MannVsMachineUpgrades.m_Upgrades[i];
+
+ // Same upgrade
+// if ( !V_strcmp( upgrade.szAttrib, g_MannVsMachineUpgrades.m_Upgrades[iUpgrade].szAttrib ) )
+// continue;
+
+ // Different tier
+ if ( upgrade.nTier != nTier )
+ continue;
+
+ // Wrong type
+ if ( upgrade.nUIGroup != g_MannVsMachineUpgrades.m_Upgrades[iUpgrade].nUIGroup )
+ continue;
+
+ CEconItemAttributeDefinition *pAttribDef = ItemSystem()->GetStaticDataForAttributeByName( upgrade.szAttrib );
+ if ( !pAttribDef )
+ continue;
+
+ // Can't use
+ if ( !CanUpgradeWithAttrib( pTFPlayer, iItemSlot, pAttribDef->GetDefinitionIndex(), &upgrade ) )
+ continue;
+
+ if ( upgrade.nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_ITEM )
+ {
+ pItem = CTFPlayerSharedUtils::GetEconItemViewByLoadoutSlot( pTFPlayer, iItemSlot );
+ if ( pItem )
+ {
+ ::FindAttribute_UnsafeBitwiseCast< attrib_value_t >( pItem->GetAttributeList(), pAttribDef, &flValue );
+ }
+ }
+ else if ( upgrade.nUIGroup == UIGROUP_UPGRADE_ATTACHED_TO_PLAYER )
+ {
+ ::FindAttribute_UnsafeBitwiseCast< attrib_value_t >( pTFPlayer->GetAttributeList(), pAttribDef, &flValue );
+ }
+
+ if ( flValue > 0.f )
+ {
+ bIsAvailable = false;
+ break;
+ }
+ }
+
+ return bIsAvailable;
+}
+
+#ifdef GAME_DLL
+//-----------------------------------------------------------------------------
+// Helper Functions
+//-----------------------------------------------------------------------------
+void CTFGameRules::SetNextMvMPopfile ( const char * next )
+{
+ s_strNextMvMPopFile = next;
+}
+
+//-----------------------------------------------------------------------------
+const char * CTFGameRules::GetNextMvMPopfile ( )
+{
+ return s_strNextMvMPopFile.Get();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameRules::BalanceTeams( bool bRequireSwitcheesToBeDead )
+{
+ // are we playing a managed match via matchmaking?
+ if ( GetMatchGroupDescription( GetCurrentMatchGroup() ) )
+ return;
+
+ if ( mp_autoteambalance.GetInt() == 2 )
+ return;
+
+ BaseClass::BalanceTeams( bRequireSwitcheesToBeDead );
+}
+#endif
+