diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/shared/tf/tf_gamerules.cpp | |
| download | archived-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.cpp | 21410 |
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 + |