Raised This Month: $32 Target: $400
 8% 

[L4D] Competitive Stats New grammar help


Post New Thread Reply   
 
Thread Tools Display Modes
Author Message
ZBzibing
Senior Member
Join Date: Dec 2012
Old 11-14-2021 , 00:53   [L4D] Competitive Stats New grammar help
Reply With Quote #1

Can someone support his new syntax, and it can run on L4D1. SM110 can no longer be compiled.
Code:
/**
 * =============================================================================
 * L4D Competitive Stats
 * Track and display various round-based survivor and infected statistics.
 * 
 * - Griffin and Philogl
 * =============================================================================
 */

public Plugin:myinfo =
{
	name = "L4D Competitive Stats",
	author = "Griffin & Philogl",
	description = "Basic competitive stat tracking on a per map basis",
	version = "0.4.2"
};

#pragma semicolon 1

#include <sourcemod>
#include include/sdkhooks.inc

#define MAXENTITIES 2048
#define SAYTEXT_MAXLENGTH 192
#define HIGHCHAR "*"
#define LOWCHAR "_"
#define TEAM_SPECTATOR 1
#define TEAM_SURVIVOR 2
#define TEAM_INFECTED 3
#define FLAG_SPECTATOR (1 << TEAM_SPECTATOR)
#define FLAG_SURVIVOR (1 << TEAM_SURVIVOR)
#define FLAG_INFECTED (1 << TEAM_INFECTED)
#define SM_REPLY_TO_CHAT ReplySource:1
#define MIN_DP_RATIO 0.8 // % of maximum DP damage to consider a DP, maybe make this a cvar?
#define BOOMER_STAGGER_TIME 4.0 // Amount of time after a boomer has been meleed that we consider the meleer the person who
								// shut down the boomer, this is just a guess value...

#define GetModifierChar(%0,%1) (%0 == lows_highs[%1][1] ? HIGHCHAR:%0 == lows_highs[%1][0] ? LOWCHAR:"")
#define GetModifierCharReversed(%0,%1) (%0 == lows_highs[%1][0] ? HIGHCHAR:%0 == lows_highs[%1][1] ? LOWCHAR:"")
#define IsSpectator(%0) (GetClientTeam(%0) == TEAM_SPECTATOR)
#define IsSurvivor(%0) (GetClientTeam(%0) == TEAM_SURVIVOR)
#define IsInfected(%0) (GetClientTeam(%0) == TEAM_INFECTED)
#define IsWitch(%0) (g_bIsWitch[%0])
#define IsPouncing(%0) (g_bIsPouncing[%0])
#define IsIncapped(%0) (GetEntProp(%0, Prop_Send, "m_isIncapacitated") > 0)
#define IsBoomed(%0) ((GetEntPropFloat(%0, Prop_Send, "m_vomitStart") + 20.1) > GetGameTime())

enum _:ZOMBIECLASS
{
	ZC_SMOKER = 1,
	ZC_BOOMER,
	ZC_HUNTER,
	ZC_WITCH,
	ZC_TANK
}

enum _:STATS
{
	FullSkeets,
	TeamSkeets,
	SkeetAssists,
	Deadstops,
	PouncesEaten,
	DPsEaten,
	CIKills,
	SIKills,
	FF,
	SIDamage,
	CIDamageTaken,
	SIDamageTaken,
	BoomerShutdowns,
	BoomAttempts,
	BoomSuccesses,
	BoomedSurvivorsByVomit,
	BoomedSurvivorsByProxy,
	PouncesLanded,
	DPsLanded,
	Skeeted,
	Deadstopped,
	DamageDealtAsSI,
	STATS_MAX
}

// Cvar related
new				g_iMaxPlayerZombies							= 4;
new				g_iSurvivorLimit							= 4;
new				g_iMinDPDamage								= 20;
new				g_iWitchHealth								= 1000;	// Default
new		Handle:	g_hCvarMaxPlayerZombies						= INVALID_HANDLE;
new		Handle:	g_hCvarSurvivorLimit						= INVALID_HANDLE;
new		Handle:	g_hCvarMaxPounceBonusDamage					= INVALID_HANDLE;
new		Handle:	g_hCvarWitchHealth							= INVALID_HANDLE;
new		Handle:	g_hCvarDirectorReadyDuration				= INVALID_HANDLE;

// Global state
new		bool:	g_bShouldAnnounceWitchDamage				= false;
new		bool:	g_bHasRoundEnded							= false;
new		bool:	g_bLogFF									= false;
new		Handle:	g_hBoomerShoveTimer							= INVALID_HANDLE;

// Player/Entity state
new				g_iAccumulatedWitchDamage;							// Current witch health = witch health - accumulated
new				g_iBoomerClient;									// Client of last player to be boomer (or current boomer)
new				g_iBoomerKiller;									// Client who shot the boomer
new				g_iBoomerShover;									// Client who shoved the boomer
new				g_iLastHealth[MAXPLAYERS + 1];
new		bool:	g_bHasBoomLanded;
new		bool:	g_bStatsCooldown[MAXPLAYERS + 1];					// Prevent spam of stats command (potential DoS vector I think)
new		bool:	g_bHasLandedPounce[MAXPLAYERS + 1];					// Used to determine if a deadstop was 'pierced'
new		bool:	g_bIsWitch[MAXENTITIES];							// Membership testing for fast witch checking
new		bool:	g_bIsPouncing[MAXPLAYERS + 1];
new		bool:	g_bShotCounted[MAXPLAYERS + 1][MAXPLAYERS +1];		// Victim - Attacker, used by playerhurt and weaponfired

// Map Stats, array for each player for easy trie storage
new				g_iMapStats[MAXPLAYERS + 1][STATS_MAX];

// Player temp stats
new				g_iWitchDamage[MAXPLAYERS + 1];
new				g_iDamageDealt[MAXPLAYERS + 1][MAXPLAYERS + 1];			// Victim - Attacker
new				g_iShotsDealt[MAXPLAYERS + 1][MAXPLAYERS + 1];			// Victim - Attacker, count # of shots (not pellets)

public OnPluginStart()
{
	if (GetMaxEntities() > MAXENTITIES)
	{
		LogError("Plugin needs to be recompiled with a new MAXENTITIES value of %d. Current value is %d. Witch tracking is unreliable!",
			GetMaxEntities(), MAXENTITIES);
	}

	for (new client = 1; client <= MaxClients; client++)
	{
		if (!IsClientInGame(client)) continue;
		SDKHook(client, SDKHook_OnTakeDamage, PlayerHook_OnTakeDamagePre);
	}

	HookEvent("round_start", Event_RoundStart);
	HookEvent("round_end", Event_RoundEnd);

	HookEvent("player_hurt", Event_PlayerHurt);
	HookEvent("player_shoved", Event_PlayerShoved);
	HookEvent("player_spawn", Event_PlayerSpawn);
	HookEvent("player_death", Event_PlayerDeath);
	HookEvent("infected_death", Event_InfectedDeath);
	HookEvent("weapon_fire", Event_WeaponFire);
	// Witch tracking
	HookEvent("player_incapacitated", Event_PlayerIncapacitated);
	HookEvent("infected_hurt", Event_InfectedHurt);
	HookEvent("witch_killed", Event_WitchKilled);
	HookEvent("witch_spawn", Event_WitchSpawn);
	// Pounce tracking
	HookEvent("ability_use", Event_AbilityUse);
	HookEvent("lunge_pounce", Event_LungePounce);
	// Boomer tracking
	HookEvent("player_now_it", Event_PlayerBoomed);

	g_hCvarMaxPlayerZombies = FindConVar("z_max_player_zombies");
	g_hCvarSurvivorLimit = FindConVar("survivor_limit");
	g_hCvarMaxPounceBonusDamage = FindConVar("z_hunter_max_pounce_bonus_damage");
	g_hCvarWitchHealth = FindConVar("z_witch_health");
	g_hCvarDirectorReadyDuration = FindConVar("director_ready_duration");

	HookConVarChange(g_hCvarMaxPlayerZombies, Cvar_MaxPlayerZombies);
	HookConVarChange(g_hCvarSurvivorLimit, Cvar_SurvivorLimit);
	HookConVarChange(g_hCvarMaxPounceBonusDamage, Cvar_MaxPounceBonusDamage);
	HookConVarChange(g_hCvarWitchHealth, Cvar_WitchHealth);
	HookConVarChange(g_hCvarDirectorReadyDuration, Cvar_DirectorReadyDuration);

	g_iMaxPlayerZombies = GetConVarInt(g_hCvarMaxPlayerZombies);
	g_iSurvivorLimit = GetConVarInt(g_hCvarSurvivorLimit);
	g_iWitchHealth = GetConVarInt(g_hCvarWitchHealth);
	if (GetConVarInt(g_hCvarDirectorReadyDuration) > 0) g_bLogFF = true;
	CalculateMinDPDamage(GetConVarFloat(g_hCvarMaxPounceBonusDamage));

	// RegConsoleCmd("sm_stats", Command_Stats, "Prints the client's stats for the current round");
}

public OnClientPutInServer(client)
{
	SDKHook(client, SDKHook_OnTakeDamage, PlayerHook_OnTakeDamagePre);
}

public OnMapStart()
{
	g_bHasRoundEnded = false;
	ClearMapStats();
}


public Action:Timer_DelayedStatsPrint(Handle:timer)
{
	PrintMVPAndTeamStats();
	PrintConsoleStats();
}

/*
MVP - SI: Philogl (2932 dmg[99%], 53 kills [100%]) Your SI rank: #4 (3 dmg [1%], 0 kills [0%])
MVP - CI: Philogl (232 common [90%]) Your CI rank: #4 (1 kills [0%])
*/

public PrintMVPAndTeamStats()
{
	decl survivor_clients[g_iSurvivorLimit];
	decl i;
	new survivor_count;
	for (i = 1; i <= MaxClients; i++)
	{
		if (!IsClientInGame(i) || !IsSurvivor(i)) continue;
		survivor_clients[survivor_count++] = i;
	}

	decl sortable[survivor_count][2];
	decl client, val;
	new total, totalkills, percent;

	// --------------------------- SI Damage ---------------------------
	for (i = 0; i < survivor_count; i++)
	{
		client = survivor_clients[i];
		val = g_iMapStats[client][SIDamage];
		sortable[i][0] = client;
		sortable[i][1] = val;
		total += val;
		totalkills += g_iMapStats[client][SIKills];
	}
	if (total == 0 || totalkills == 0)
	{
		PrintToChatAll("\x01MVP - SI: \x03N/A\x01");
	}
	else
	{
		SortCustom2D(sortable, survivor_count, ClientValue2DSortDesc);
		client = sortable[0][0];
		val = sortable[0][1];
		percent = RoundFloat((float(val) / float(total)) * 100.0);
		new kills = g_iMapStats[client][SIKills];
		// This string (colors, etc) is stolen wholesale from Tabun's L4D2 MVP plugin, credit to him
		PrintToChatAll("\x01MVP - SI: \x03%N\x01 (\x05%d\x01 dmg [\x04%d%%\x01], \x05%d\x01 kills [\x04%d%%\x01])",
			client, val, percent, kills,
			RoundFloat((float(kills) / float(totalkills)) * 100.0));

		// Print individual reports for each survivor that isn't the MVP
		for (i = 1; i < survivor_count; i++)
		{
			client = sortable[i][0];
			val = sortable[i][1];
			kills = g_iMapStats[client][SIKills];
			percent = RoundFloat((float(val) / float(total)) * 100.0);
			PrintToChat(client, "\x01You - SI: \x05#%d\x01 (\x05%d\x01 dmg [\x04%d%%\x01], \x05%d\x01 kills [\x04%d%%\x01]", i + 1, val, percent, kills,
				RoundFloat((float(kills) / float(totalkills)) * 100.0));
		}
	}

	// --------------------------- CI Kills ---------------------------
	total = 0;
	for (i = 0; i < survivor_count; i++)
	{
		client = survivor_clients[i];
		val = g_iMapStats[client][CIKills];
		sortable[i][0] = client;
		sortable[i][1] = val;
		total += val;
	}
	if (total == 0)
	{
		PrintToChatAll("\x01MVP - CI: \x03N/A\x01");
	}
	else
	{
		SortCustom2D(sortable, survivor_count, ClientValue2DSortDesc);
		client = sortable[0][0];
		val = sortable[0][1];
		percent = RoundFloat((float(val) / float(total)) * 100.0);
		// Again, credit to Tabun
		PrintToChatAll("\x01MVP - CI: \x03%N\x01 (\x05%d\x01 common [\x04%d%%\x01])",
			client, val, percent);

		for (i = 1; i < survivor_count; i++)
		{
			client = sortable[i][0];
			val = sortable[i][1];
			percent = RoundFloat((float(val) / float(total)) * 100.0);
			PrintToChat(client, "\x01You - CI: \x05#%d\x01 (\x05%d\x01 common [\x04%d%%\x01])",
				i + 1, val, percent);
		}
	}

	// --------------------------- Team Stats ---------------------------
	new skeets, fullskeets, teamskeets, deadstops, pounce_eats, dp_eats;
	new successful_booms, shutdown_booms, vomited_survivors, proxied_survivors;
	new dmg_from_si, dmg_from_ci, dmg_from_ff;

	for (i = 1; i <= MaxClients; i++)
	{
		if (!IsClientInGame(i)) continue;
		if (IsSurvivor(i))
		{
			fullskeets += g_iMapStats[i][FullSkeets];
			teamskeets += g_iMapStats[i][TeamSkeets];
			skeets += g_iMapStats[i][FullSkeets] + g_iMapStats[i][TeamSkeets];
			deadstops += g_iMapStats[i][Deadstops];
			pounce_eats += g_iMapStats[i][PouncesEaten];
			dp_eats += g_iMapStats[i][DPsEaten];
			shutdown_booms += g_iMapStats[i][BoomerShutdowns];
			dmg_from_si += g_iMapStats[i][SIDamageTaken];
			dmg_from_ci += g_iMapStats[i][CIDamageTaken];
			dmg_from_ff += g_iMapStats[i][FF];
		}
		else if (IsInfected(i))
		{
			successful_booms += g_iMapStats[i][BoomSuccesses];
			vomited_survivors += g_iMapStats[i][BoomedSurvivorsByVomit];
			proxied_survivors += g_iMapStats[i][BoomedSurvivorsByProxy];
		}
	}

/*
TEAM - Hunters: 40% (4/10) shutdown (3 skeets, 1 DS, 2 DPs landed)
TEAM - Boomers: 20% (1/5) shutdown (4 vomited, 3 proxied)
TEAM - Damage: 173 from SI, 97 from common, 23 from FF
*/

	if ((skeets + deadstops + pounce_eats) == 0)
	{
		PrintToChatAll("\x01TEAM - Hunters: \x03N/A\x01");
	}
	else
	{
		PrintToChatAll("\x01TEAM - Hunters: \x04%d%%\x01 (\x05%d\x01/\x05%d\x01) shutdown (\x05%d\x01 skeet%s, \x05%d\x01 DS, \x05%d\x01 DP%s landed)",
			RoundFloat((float(skeets + deadstops) / float(skeets + deadstops + pounce_eats)) * 100.0),
			(skeets + deadstops), (skeets + deadstops + pounce_eats),
			skeets, skeets == 1 ? "":"s", deadstops, dp_eats, dp_eats == 1 ? "":"s");
	}

	if ((successful_booms + shutdown_booms) == 0)
	{
		PrintToChatAll("\x01TEAM - Boomers: \x03N/A\x01");
	}
	else
	{
		PrintToChatAll("\x01TEAM - Boomers: \x04%d%%\x01 (\x05%d\x01/\x05%d\x01) shutdown (\x05%d\x01 vomited, \x05%d\x01 proxied)",
			RoundFloat((float(shutdown_booms) / float(successful_booms + shutdown_booms)) * 100.0),
			shutdown_booms, (shutdown_booms + successful_booms), vomited_survivors, proxied_survivors);
	}

	if ((dmg_from_si + dmg_from_ci + dmg_from_ff) == 0)
	{
		PrintToChatAll("\x01TEAM - Damage: \x03N/A\x01");
	}
	else
	{
		PrintToChatAll("\x01TEAM - Damage: \x05%d\x01 from \x03SI\x01, \x05%d\x01 from \x03common\x01, \x05%d\x01 from \x03FF\x01",
			dmg_from_si, dmg_from_ci, dmg_from_ff);
	}
}

// Spectators: Print survivor stats & infected stats
// Survivors: Print survivor stats
// Infected: Print infected stats
public PrintConsoleStats()
{
	CreateTimer(0.1, Timer_PrintSurvivorStatsHeader);
	CreateTimer(0.2, Timer_PrintSurvivorStatsBody);
	CreateTimer(0.3, Timer_PrintSurvivorStatsFooter);
	CreateTimer(0.4, Timer_PrintInfectedStatsHeader);
	CreateTimer(0.5, Timer_PrintInfectedStatsBody);
	CreateTimer(0.6, Timer_PrintInfectedStatsFooter);
}

public Action:Timer_PrintSurvivorStatsHeader(Handle:timer)
{
	new const maxlength = 1024;
	decl String:buf[maxlength];
	Format(buf, maxlength, "\n|----------------------------------------------- SURVIVOR STATS -----------------------------------------------|\n");
	Format(buf, maxlength, "%s| NAME                 | SIK  | SID    | CI   | DS  | Skeets         | SA  | BS  | FF   | DFC  | Pounces Eaten |\n", buf);
	Format(buf, maxlength, "%s|----------------------|------|--------|------|-----|----------------|-----|-----|------|------|---------------|", buf);
	PrintToTeamConsole(FLAG_SPECTATOR | FLAG_SURVIVOR, buf);
}

public Action:Timer_PrintSurvivorStatsBody(Handle:timer)
{
	decl i, j, val;
	decl lows_highs[STATS_MAX][2];
	// Initialize lows_highs
	for (i = 0; i < STATS_MAX; i++)
	{
		lows_highs[i][0] = 9999999;
		lows_highs[i][1] = -1;
	}

	// Calculate actual lows_highs values
	for (i = 1; i <= MaxClients; i++)
	{
		if (!IsClientInGame(i) || !IsSurvivor(i)) continue;
		for (j = 0; j < STATS_MAX; j++)
		{
			if (j == TeamSkeets) continue;
			else if (j == FullSkeets)
			{ // Store the total skeets value in fullskeets, just for checking lows_highs
				val = g_iMapStats[i][FullSkeets] + g_iMapStats[i][TeamSkeets];
			}
			else
			{
				val = g_iMapStats[i][j];
			}
			if (val < lows_highs[j][0]) lows_highs[j][0] = val;
			if (val > lows_highs[j][1]) lows_highs[j][1] = val;
		}
	}

	new const max_name_len = 20;
	new const s_len = 15;
	decl String:name[MAX_NAME_LENGTH];
	decl String:sikills[s_len], String:sidamage[s_len], String:cikills[s_len], String:deadstops[s_len];
	decl String:skeets[s_len], String:skeetassists[s_len], String:boomershutdowns[s_len];
	decl String:ff[s_len], String:cidamage[s_len], String:pounceseaten[s_len];

	for (i = 1; i <= MaxClients; i++)
	{
		if (!IsClientInGame(i) || !IsSurvivor(i)) continue;
		GetClientName(i, name, sizeof(name));
		name[max_name_len] = 0;
		val = g_iMapStats[i][SIKills];
		Format(sikills, s_len, "%s%d", GetModifierChar(val, SIKills), val);

		val = g_iMapStats[i][SIDamage];
		Format(sidamage, s_len, "%s%d", GetModifierChar(val, SIDamage), val);

		val = g_iMapStats[i][CIKills];
		Format(cikills, s_len, "%s%d", GetModifierChar(val, CIKills), val);

		val = g_iMapStats[i][Deadstops];
		Format(deadstops, s_len, "%s%d", GetModifierChar(val, Deadstops), val);

		val = g_iMapStats[i][FullSkeets] + g_iMapStats[i][TeamSkeets];
		Format(skeets, s_len, "%s%d (%dF/%dT)", GetModifierChar(val, FullSkeets), val,
													g_iMapStats[i][FullSkeets], g_iMapStats[i][TeamSkeets]);

		val = g_iMapStats[i][SkeetAssists];
		Format(skeetassists, s_len, "%s%d", GetModifierChar(val, SkeetAssists), val);

		val = g_iMapStats[i][BoomerShutdowns];
		Format(boomershutdowns, s_len, "%s%d", GetModifierChar(val, BoomerShutdowns), val);

		val = g_iMapStats[i][FF];
		Format(ff, s_len, "%s%d", GetModifierCharReversed(val, FF), val);

		val = g_iMapStats[i][CIDamageTaken];
		Format(cidamage, s_len, "%s%d", GetModifierCharReversed(val, CIDamageTaken), val);

		val = g_iMapStats[i][PouncesEaten];
		Format(pounceseaten, s_len, "%s%d (%d DPs)", GetModifierCharReversed(val, PouncesEaten), val, g_iMapStats[i][DPsEaten]);

		PrintToTeamConsole(FLAG_SPECTATOR | FLAG_SURVIVOR,
			"| %20s | %4s | %6s | %4s | %3s | %14s | %3s | %3s | %4s | %4s | %13s |",
			name,
			sikills,
			sidamage,
			cikills,
			deadstops,
			skeets,
			skeetassists,
			boomershutdowns,
			ff,
			cidamage,
			pounceseaten);
	}
}

public Action:Timer_PrintSurvivorStatsFooter(Handle:timer)
{
	new const maxlength = 1024;
	decl String:buf[maxlength];
	Format(buf, maxlength, "\nLegend:\n");
	Format(buf, maxlength, "%s  %s = Best  %s = Worst\n", buf, HIGHCHAR, LOWCHAR);
	Format(buf, maxlength, "%s  SIK     - Special Infected killed\n", buf);
	Format(buf, maxlength, "%s  SID     - Damage dealt to Special Infected\n", buf);
	Format(buf, maxlength, "%s  CI      - Common infected killed\n", buf);
	Format(buf, maxlength, "%s  DS      - Deadstops landed\n", buf);
	Format(buf, maxlength, "%s  Skeets  - Total number of skeets (# full skeets/# team skeets)\n", buf);
	Format(buf, maxlength, "%s  SA      - Skeets assisted\n", buf);
	Format(buf, maxlength, "%s  BS      - Boomer shutdowns\n", buf);
	Format(buf, maxlength, "%s  FF      - Friendly Fire\n", buf);
	Format(buf, maxlength, "%s  DFC     - Damage from common infected\n", buf);
	PrintToTeamConsole(FLAG_SPECTATOR | FLAG_SURVIVOR, buf);
}

public Action:Timer_PrintInfectedStatsHeader(Handle:timer)
{
	new const maxlength = 1024;
	decl String:buf[maxlength];
	Format(buf, maxlength, "\n|----------------------------------- INFECTED STATS -----------------------------------|\n");
	Format(buf, maxlength, "%s| NAME                 | Dmg   | Pounce Success            | Boomer Success            |\n", buf);
	Format(buf, maxlength, "%s|----------------------|-------|---------------------------|---------------------------|", buf);
//| Name | *1291 | *100% (10L (2DP)/10S/10D) | *100% (10A/13F/10V/10P) |
	PrintToTeamConsole(FLAG_SPECTATOR | FLAG_INFECTED, buf);
}

public Action:Timer_PrintInfectedStatsBody(Handle:timer)
{
	new dmg_low = 99999999;
	new dmg_high = -1;
	new pounces_low = 99999999;
	new pounces_high = -1;
	new booms_low = 99999999;
	new booms_high = -1;

	decl val, i;
	new bool:has_printed;

	for (i = 1; i <= MaxClients; i++)
	{
		if (!IsClientInGame(i) || !IsInfected(i) || IsFakeClient(i)) continue;
		val = g_iMapStats[i][DamageDealtAsSI];
		if (val < dmg_low) dmg_low = val;
		if (val > dmg_high) dmg_high = val;

		val = g_iMapStats[i][PouncesLanded];
		if (val < pounces_low) pounces_low = val;
		if (val > pounces_high) pounces_high = val;

		val = g_iMapStats[i][BoomSuccesses];
		if (val < booms_low) booms_low = val;
		if (val > booms_high) booms_high = val;
	}

	new const max_name_len = 20;
	new const s_len = 30;
	decl String:name[MAX_NAME_LENGTH];
	decl String:dmg[s_len], String:pounce_success[s_len], String:boomer_success[s_len];

	for (i = 1; i <= MaxClients; i++)
	{
		// No infected bot stats...
		if (!IsClientInGame(i) || !IsInfected(i) || IsFakeClient(i)) continue;
		GetClientName(i, name, sizeof(name));
		name[max_name_len] = 0;

		val = g_iMapStats[i][DamageDealtAsSI];
		Format(dmg, s_len, "%s%d",
			val == dmg_high ? HIGHCHAR:val == dmg_low ? LOWCHAR:"",
			val);

		val = g_iMapStats[i][PouncesLanded];
		Format(pounce_success, s_len, "%s%d/%d (%d DPs/%d S/%d DS)",
			val == pounces_high ? HIGHCHAR:val == pounces_low ? LOWCHAR:"",
			val,
			val + g_iMapStats[i][Skeeted] + g_iMapStats[i][Deadstopped],
			g_iMapStats[i][DPsLanded],
			g_iMapStats[i][Skeeted],
			g_iMapStats[i][Deadstopped]);

		val = g_iMapStats[i][BoomSuccesses];
		Format(boomer_success, s_len, "%s%d/%d (%d Vomit/%d Proxy)",
			val == booms_high ? HIGHCHAR:val == booms_low ? LOWCHAR:"",
			val,
			g_iMapStats[i][BoomAttempts],
			g_iMapStats[i][BoomedSurvivorsByVomit],
			g_iMapStats[i][BoomedSurvivorsByProxy]);

		PrintToTeamConsole(FLAG_SPECTATOR | FLAG_INFECTED,
			"| %20s | %5s | %25s | %25s |",
			name,
			dmg,
			pounce_success,
			boomer_success);

		has_printed = true;
	}

	if (!has_printed)
	{
		PrintToTeamConsole(FLAG_SPECTATOR | FLAG_INFECTED, "No infected found.");
	}
}

public Action:Timer_PrintInfectedStatsFooter(Handle:timer)
{
	new const maxlength = 1024;
	decl String:buf[maxlength];
	Format(buf, maxlength, "\nLegend:\n");
	Format(buf, maxlength, "%s   Dmg              - Damage dealt to non-incapped survivors\n", buf);
	Format(buf, maxlength, "%s   Pounce Success   - DP = Damage Pounce, S = Skeeted, DS = Deadstop\n", buf);
	PrintToTeamConsole(FLAG_SPECTATOR | FLAG_INFECTED, buf);
}

/*
public Action:Command_Stats(client, args)
{
	if (g_bStatsCooldown[client]) return Plugin_Handled;
	g_bStatsCooldown[client] = true;
	decl String:name[MAX_NAME_LENGTH];
	GetClientName(client, name, sizeof(name));
	if (StrEqual(name, "Griffin"))
	{
		PrintMVPAndTeamStats();
		PrintConsoleStats();
	}
	else
	{
		ReplyToCommand(client, "[SM] This command is currently disabled, stats will print automatically at the end of the round.");
	}
	// PrintToChatAll("[DEBUG] Printing stats to all!\n");
	// PrintStatsToAll();
	// if (GetCmdReplySource() == SM_REPLY_TO_CHAT) PrintToChat(client, "[SM] Check console for output.\n");
	CreateTimer(1.0, Timer_StatsCooldown, client);
	return Plugin_Handled;
}
*/

public Action:Timer_StatsCooldown(Handle:timer, any:client)
{
	g_bStatsCooldown[client] = false;
	return Plugin_Stop;
}

public Cvar_MaxPlayerZombies(Handle:convar, const String:oldValue[], const String:newValue[])
{
	g_iMaxPlayerZombies = StringToInt(newValue);
}

public Cvar_SurvivorLimit(Handle:convar, const String:oldValue[], const String:newValue[])
{
	g_iSurvivorLimit = StringToInt(newValue);
}

public Cvar_MaxPounceBonusDamage(Handle:convar, const String:oldValue[], const String:newValue[])
{
	CalculateMinDPDamage(StringToFloat(newValue));
}

CalculateMinDPDamage(Float:bonus_pounce_damage)
{
	// Max pounce damage = bonus pounce damage + 1
	g_iMinDPDamage = RoundToFloor((bonus_pounce_damage + 1.0) * MIN_DP_RATIO);
}

public Cvar_WitchHealth(Handle:convar, const String:oldValue[], const String:newValue[])
{
	g_iWitchHealth = StringToInt(newValue);
}

public Cvar_DirectorReadyDuration(Handle:convar, const String:oldValue[], const String:newValue[])
{
	g_bLogFF = StringToInt(newValue) > 0 ? true:false;
}

public Event_RoundStart(Handle:event, const String:name[], bool:dontBroadcast)
{
	g_bHasRoundEnded = false;
}

public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast)
{
	// In case witch is avoided
	g_iAccumulatedWitchDamage = 0;
	ResetWitchTracking();
	if (g_bHasRoundEnded) return;
	g_bHasRoundEnded = true;
	CreateTimer(2.0, Timer_DelayedStatsPrint);
	for (new i = 1; i <= MaxClients; i++)
	{
		// if (IsClientInGame(i) && IsSurvivor(i))
		// {
			// buf = GetStatString(i);
			// PrintToChat(i, "Round ended.");
		// }
		ClearDamage(i);
		g_iWitchDamage[i] = 0;
	}
}

public Action:PlayerHook_OnTakeDamagePre(victim, &attacker, &inflictor, &Float:damage, &damagetype)
{
	// Non incapped survivor victim
	if (!victim ||
		victim > MaxClients ||
		!IsClientInGame(victim) ||
		!IsSurvivor(victim) ||
		IsIncapped(victim)
		) return;

	g_iLastHealth[victim] = GetClientHealth(victim);
}

public Event_PlayerHurt(Handle:event, const String:name[], bool:dontBroadcast)
{
	if (g_bHasRoundEnded) return;
	new victim = GetClientOfUserId(GetEventInt(event, "userid"));

	if (victim == 0 ||
		!IsClientInGame(victim)
		) return;

	new attacker = GetClientOfUserId(GetEventInt(event, "attacker"));
	if (!attacker)
	{
		// Damage from common
		if (!IsCommonInfected(GetEventInt(event, "attackerentid")) || IsIncapped(victim)) return;
		new damage = g_iLastHealth[victim] - GetEventInt(event, "health");
		if (damage < 0 || damage > 2)
		{
			PrintToChatAll("[DEBUG] Invalid common damage value of %d detected for %N. Tell Griffin what happened!",
				damage,
				victim);
			damage = 1;
		}
		g_iMapStats[victim][CIDamageTaken] += damage;

		if (IsBoomed(victim) &&
			g_iBoomerClient &&
			IsClientInGame(g_iBoomerClient) &&
			!IsFakeClient(g_iBoomerClient))
		{
			g_iMapStats[g_iBoomerClient][DamageDealtAsSI] += damage;
		}
		return;
	}
	else if (!IsClientInGame(attacker)) return;

	new damage = GetEventInt(event, "dmg_health");

	if (IsSurvivor(attacker))
	{
		// FF (don't log incapped damage, doesn't matter)
		if (IsSurvivor(victim) && !IsIncapped(victim) && g_bLogFF)
		{
			g_iMapStats[attacker][FF] += damage;
		}
		// Hot survivor on infected action, baby
		else if (IsInfected(victim))
		{
			new zombieclass = GetEntProp(victim, Prop_Send, "m_zombieClass");
			if (zombieclass == ZC_TANK) return; // We don't care about tank damage

			if (!g_bShotCounted[victim][attacker])
			{
				g_iShotsDealt[victim][attacker]++;
				g_bShotCounted[victim][attacker] = true;
			}

			new remaining_health = GetEventInt(event, "health");

			// Let player_death handle remainder damage (avoid overkill damage)
			if (remaining_health <= 0) return;

			// remainder health will be awarded as damage on kill
			g_iLastHealth[victim] = remaining_health;

			g_iMapStats[attacker][SIDamage] += damage;
			g_iDamageDealt[victim][attacker] += damage;

			if (zombieclass == ZC_BOOMER)
			{ /* Boomer Stuff Here */ }
			else if (zombieclass == ZC_HUNTER)
			{ /* Hunter Stuff Here */ }
		}
	}
	if (IsInfected(attacker) && IsSurvivor(victim) && !IsIncapped(victim))
	{
		g_iMapStats[victim][SIDamageTaken] += damage;
		g_iMapStats[attacker][DamageDealtAsSI] += damage;
	}
}

public Event_PlayerShoved(Handle:event, const String:name[], bool:dontBroadcast)
{
	if (g_bHasRoundEnded) return;
	new victim = GetClientOfUserId(GetEventInt(event, "userid"));
	if (victim == 0 ||
		!IsClientInGame(victim) ||
		!IsInfected(victim)
		) return;

	new attacker = GetClientOfUserId(GetEventInt(event, "attacker"));
	if (attacker == 0 ||				// World dmg?
		!IsClientInGame(attacker) ||	// Unsure
		!IsSurvivor(attacker)
		) return;

	new zombieclass = GetEntProp(victim, Prop_Send, "m_zombieClass");
	if (zombieclass == ZC_BOOMER)
	{
		if (g_hBoomerShoveTimer != INVALID_HANDLE)
		{
			KillTimer(g_hBoomerShoveTimer);
			if (!g_iBoomerShover || !IsClientInGame(g_iBoomerShover)) g_iBoomerShover = attacker;
		}
		else
		{
			g_iBoomerShover = attacker;
		}
		g_hBoomerShoveTimer = CreateTimer(BOOMER_STAGGER_TIME, Timer_BoomerShove);
	}
	else if (zombieclass == ZC_HUNTER && IsPouncing(victim))
	{ // DEADSTOP

		// Groundtouch timer will do this for us, but
		// this prevents multiple deadstops being counted incorrectly
		g_bIsPouncing[victim] = false;
		// Delayed check to see if the pounce actually landed due to bug where player_shoved gets fired but pounce lands anyways
		g_bHasLandedPounce[attacker] = false;
		new Handle:pack;
		CreateDataTimer(0.2, Timer_DeadstopCheck, pack);
		WritePackCell(pack, attacker);
		WritePackCell(pack, victim);
	}
}

public Action:Timer_DeadstopCheck(Handle:timer, Handle:pack)
{
	ResetPack(pack);
	new attacker = ReadPackCell(pack);
	if (!g_bHasLandedPounce[attacker])
	{
		new victim = ReadPackCell(pack);
		// TODO: Add tracking for number of times a person was deadstopped (along with other pounce stats)
		g_iMapStats[attacker][Deadstops]++;
		if (IsClientInGame(victim) && IsClientInGame(attacker))
		{
			PrintToChat(attacker, "[SM] You deadstopped %N.", victim);
			if (!IsFakeClient(victim))
			{
				g_iMapStats[victim][Deadstopped]++;
				PrintToChat(victim, "[SM] You were deadstopped by %N.", attacker);
			}
		}
	}
}

public Action:Timer_BoomerShove(Handle:timer)
{
	// PrintToChatAll("[DEBUG] BoomerShove timer expired, credit for boomer shutdown is available to anyone at this point!");
	g_hBoomerShoveTimer = INVALID_HANDLE;
	g_iBoomerShover = 0;
}

public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast)
{
	new client = GetClientOfUserId(GetEventInt(event, "userid"));
	if (client == 0 || !IsClientInGame(client)) return;

	if (IsInfected(client))
	{
		new zombieclass = GetEntProp(client, Prop_Send, "m_zombieClass");
		if (zombieclass == ZC_TANK) return;

		if (zombieclass == ZC_BOOMER)
		{
			// Fresh boomer spawning (if g_iBoomerClient is set and an AI boomer spawns, it's a boomer going AI)
			if (!IsFakeClient(client) || !g_iBoomerClient)
			{
				g_bHasBoomLanded = false;
				g_iBoomerClient = client;
				g_iBoomerShover = 0;
				g_iBoomerKiller = 0;
			}
			if (!IsFakeClient(client))
			{
				g_iMapStats[client][BoomAttempts]++;
			}
			if (g_hBoomerShoveTimer != INVALID_HANDLE)
			{
				KillTimer(g_hBoomerShoveTimer);
				g_hBoomerShoveTimer = INVALID_HANDLE;
			}
		}

		g_iLastHealth[client] = GetClientHealth(client);
	}
}

public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast)
{
	if (g_bHasRoundEnded) return;
	new victim = GetClientOfUserId(GetEventInt(event, "userid"));

	if (victim == 0 ||
		!IsClientInGame(victim)
		) return;

	new attacker = GetClientOfUserId(GetEventInt(event, "attacker"));

	if (attacker == 0)
	{ // Check for a witch-related death (black & white survivor failing or no-incap configs e.g. 1v1)
		if (IsInfected(victim)) ClearDamage(victim);
		if (!IsWitch(GetEventInt(event, "attackerentid")) ||
			!g_bShouldAnnounceWitchDamage					// Prevent double print on incap -> death by witch
			) return;
		
		new health = g_iWitchHealth - g_iAccumulatedWitchDamage;
		if (health < 0) health = 0;

		PrintToChatAll("[SM] Witch had %d health remaining.", health);
		for (new i = 1; i <= MaxClients; i++)
		{
			if (g_iWitchDamage[i] > 0 && IsClientInGame(i))
			{
				PrintToChat(i, "[SM] You dealt %d damage to the witch.", g_iWitchDamage[i]);
			}
		}
		g_iAccumulatedWitchDamage = 0;
		g_bShouldAnnounceWitchDamage = false;
		return;
	}

	if (!IsClientInGame(attacker))
	{
		if (IsInfected(victim)) ClearDamage(victim);
		return;
	}

	if (IsSurvivor(attacker) && IsInfected(victim))
	{
		new zombieclass = GetEntProp(victim, Prop_Send, "m_zombieClass");
		if (zombieclass == ZC_TANK) return; // We don't care about tank damage
	
		g_iMapStats[attacker][SIKills]++;
		new lasthealth = g_iLastHealth[victim];
		g_iMapStats[attacker][SIDamage] += lasthealth;
		g_iDamageDealt[victim][attacker] += lasthealth;
		if (zombieclass == ZC_BOOMER)
		{
			// Only happens on mid map plugin load when a boomer is up
			if (!g_iBoomerClient) g_iBoomerClient = victim;

			CreateTimer(0.2, Timer_BoomerKilledCheck, victim);
			g_iBoomerKiller = attacker;
		}
		else if (zombieclass == ZC_HUNTER && IsPouncing(victim))
		{ // Skeet!
			if (!IsFakeClient(victim))
			{
				g_iMapStats[victim][Skeeted]++;
			}
			decl assisters[g_iSurvivorLimit][2];
			new assister_count, i;
			new damage = g_iDamageDealt[victim][attacker];
			new shots = g_iShotsDealt[victim][attacker];
			new String:plural[1] = "s";
			if (shots == 1) plural[0] = 0;
			for (i = 1; i <= MaxClients; i++)
			{
				if (i == attacker) continue;
				if (g_iDamageDealt[victim][i] > 0 && IsClientInGame(i))
				{
					g_iMapStats[i][SkeetAssists]++;
					assisters[assister_count][0] = i;
					assisters[assister_count][1] = g_iDamageDealt[victim][i];
					assister_count++;
				}
			}
			if (assister_count)
			{
				// Sort by damage, descending
				SortCustom2D(assisters, assister_count, ClientValue2DSortDesc);
				decl String:assister_string[128];
				decl String:buf[MAX_NAME_LENGTH + 8];
				new assist_shots = g_iShotsDealt[victim][assisters[0][0]];
				// Construct assisters string
				Format(assister_string, sizeof(assister_string), "%N (%d/%d shot%s)",
					assisters[0][0],
					assisters[0][1],
					g_iShotsDealt[victim][assisters[0][0]],
					assist_shots == 1 ? "":"s");
				for (i = 1; i < assister_count; i++)
				{
					assist_shots = g_iShotsDealt[victim][assisters[i][0]];
					Format(buf, sizeof(buf), ", %N (%d/%d shot%s)",
						assisters[i][0],
						assisters[i][1],
						assist_shots,
						assist_shots == 1 ? "":"s");
					StrCat(assister_string, sizeof(assister_string), buf);
				}

				// Print to assisters
				for (i = 0; i < assister_count; i++)
				{
					PrintToChat(assisters[i][0], "[SM] %N teamskeeted %N for %d damage in %d shot%s. Assisted by: %s.",
						attacker, victim, damage, shots, plural, assister_string);
				}
				// Print to victim
				PrintToChat(victim, "[SM] You were teamskeeted by %N for %d damage in %d shot%s. Assisted by: %s.",
					attacker, damage, shots, plural, assister_string);

				// Finally print to attacker
				PrintToChat(attacker, "[SM] You teamskeeted %N for %d damage in %d shot%s. Assisted by: %s.",
					victim, damage, shots, plural, assister_string);

				g_iMapStats[attacker][TeamSkeets]++;
			}
			else
			{
				g_iMapStats[attacker][FullSkeets]++;
				PrintToChat(victim, "[SM] You were skeeted by %N in %d shot%s.", attacker, shots, plural);
				PrintToChat(attacker, "[SM] You skeeted %N in %d shot%s.", victim, shots, plural);
			}
		}
	}

	if (IsInfected(victim)) ClearDamage(victim);
}

public Action:Timer_BoomerKilledCheck(Handle:timer, any:client)
{
	// if g_iBoomerClient != client, boomer went AI, maybe do something with that info in the future?
	if (g_bHasBoomLanded) return;

	// In the following code even if it was an AI boomer that was shutdown, we're going to consider the AI boomer
	// the responsibility of the person who spawned it, aka g_iBoomerClient
	if (g_iBoomerShover && IsClientInGame(g_iBoomerShover))
	{
		if (IsClientInGame(g_iBoomerClient))
		{
			if (IsFakeClient(g_iBoomerClient))
			{
				PrintToChat(g_iBoomerShover, "[SM] You shut down an AI boomer.");
			}
			else
			{
				PrintToChat(g_iBoomerShover, "[SM] You shut down %N's boomer.", g_iBoomerClient);
				PrintToChat(g_iBoomerClient, "[SM] %N shut down your boomer.", g_iBoomerShover);
				// g_iMapStats[g_iBoomerClient][BoomFailures]++;
			}
		}
		g_iMapStats[g_iBoomerShover][BoomerShutdowns]++;
	}
	else if (IsClientInGame(g_iBoomerKiller))
	{
		if (IsClientInGame(g_iBoomerClient))
		{
			if (IsFakeClient(g_iBoomerClient))
			{
				PrintToChat(g_iBoomerKiller, "[SM] You shut down an AI boomer.");
			}
			else
			{
				PrintToChat(g_iBoomerKiller, "[SM] You shut down %N's boomer.", g_iBoomerClient);
				PrintToChat(g_iBoomerClient, "[SM] %N shut down your boomer.", g_iBoomerKiller);
				// g_iMapStats[g_iBoomerClient][BoomFailures]++;
			}
		}
		g_iMapStats[g_iBoomerKiller][BoomerShutdowns]++;
	}

	g_iBoomerClient = 0;
}

public Event_InfectedDeath(Handle:event, const String:name[], bool:dontBroadcast)
{
	if (g_bHasRoundEnded) return;
	// NOTE: Has some interesting stats like headshots, if it was a minigun kill or from explosion (might use in future)
	new attacker = GetClientOfUserId(GetEventInt(event, "attacker"));

	if (attacker == 0 ||				// Killed by world?
		!IsClientInGame(attacker) ||
		!IsSurvivor(attacker)			// Tank killing common?
		) return;

	g_iMapStats[attacker][CIKills]++;
}

public Event_WeaponFire(Handle:event, const String:name[], bool:dontBroadcast)
{
	new client = GetClientOfUserId(GetEventInt(event, "userid"));
	for (new i = 1; i <= MaxClients; i++)
	{
		// [Victim][Attacker]
		g_bShotCounted[i][client] = false;
	}
}

public Event_PlayerIncapacitated(Handle:event, const String:name[], bool:dontBroadcast)
{
	new victim = GetClientOfUserId(GetEventInt(event, "userid"));

	new attacker = GetClientOfUserId(GetEventInt(event, "attacker"));
	if (attacker && IsClientInGame(attacker) && IsInfected(attacker))
	{
		g_iMapStats[victim][SIDamageTaken] += g_iLastHealth[victim];
		g_iMapStats[attacker][DamageDealtAsSI] += g_iLastHealth[victim];
		return;
	}

	if (!IsWitch(GetEventInt(event, "attackerentid")) ||
		!g_bShouldAnnounceWitchDamage					// Prevent double print on witch incapping 2 players (rare)
		) return;

	new health = g_iWitchHealth - g_iAccumulatedWitchDamage;
	if (health < 0) health = 0;

	PrintToChatAll("[SM] Witch had %d health remaining.", health);
	for (new i = 1; i <= MaxClients; i++)
	{
		if (g_iWitchDamage[i] > 0 && IsClientInGame(i) && IsSurvivor(i))
		{
			PrintToChat(i, "[SM] You dealt %d damage to the witch.", g_iWitchDamage[i]);
		}
	}
	g_iAccumulatedWitchDamage = 0;
	g_bShouldAnnounceWitchDamage = false;
}

public Event_InfectedHurt(Handle:event, const String:name[], bool:dontBroadcast)
{
	if (g_bHasRoundEnded) return;
	new attacker = GetClientOfUserId(GetEventInt(event, "attacker"));

	if (attacker == 0 ||								// Killed by world?
		!IsWitch(GetEventInt(event, "entityid")) ||		// Tracking witch damage only
		!IsClientInGame(attacker) ||
		!IsSurvivor(attacker)							// Claws
		) return;

	new damage = GetEventInt(event, "amount");
	g_iWitchDamage[attacker] += damage;
	g_iAccumulatedWitchDamage += damage;
}

public Event_WitchKilled(Handle:event, const String:name[], bool:dontBroadcast)
{
	if (g_bHasRoundEnded) return;
	g_bIsWitch[GetEventInt(event, "witchid")] = false;

	new killer = GetClientOfUserId(GetEventInt(event, "userid"));

	if (killer == 0 ||				// Killed by world?
		!IsClientInGame(killer)
		) return;

	// Witch kills increment CI kill count, we don't want that (this seems hacky)
	if (IsSurvivor(killer)) g_iMapStats[killer][CIKills]--;

	// Not a crown, show all the survivors how they helped
	// TODO: will show someone how much damage they did on an unassisted drawcrown, fix? do we care?
	if (!GetEventBool(event, "oneshot") && g_bShouldAnnounceWitchDamage)
	{
		for (new i = 1; i <= MaxClients; i++)
		{
			if (g_iWitchDamage[i] > 0 && IsClientInGame(i) && IsSurvivor(i))
			{
				PrintToChat(i, "[SM] You dealt %d damage to the witch.", g_iWitchDamage[i]);
			}
			g_iWitchDamage[i] = 0;
		}
	}

	for (new i = 1; i <= MaxClients; i++) { g_iWitchDamage[i] = 0; }
	g_iAccumulatedWitchDamage = 0;
	g_bShouldAnnounceWitchDamage = true;
}

public Event_WitchSpawn(Handle:event, const String:name[], bool:dontBroadcast)
{
	if (g_bHasRoundEnded) return;
	g_bIsWitch[GetEventInt(event, "witchid")] = true;
	g_bShouldAnnounceWitchDamage = true;
}

// Pounce tracking, from skeet announce
public Event_AbilityUse(Handle:event, const String:name[], bool:dontBroadcast)
{
	if (g_bHasRoundEnded) return;
	new client = GetClientOfUserId(GetEventInt(event, "userid"));
	decl String:ability_name[64];

	GetEventString(event, "ability", ability_name, sizeof(ability_name));
	if (IsClientInGame(client) && strcmp(ability_name, "ability_lunge", false) == 0)
	{
		g_bIsPouncing[client] = true;
		CreateTimer(0.5, Timer_GroundedCheck, client, TIMER_REPEAT);
	}
}

public Action:Timer_GroundedCheck(Handle:timer, any:client)
{
	if (!IsClientInGame(client) || IsGrounded(client))
	{
		g_bIsPouncing[client] = false;
		KillTimer(timer);
	}
}

public Event_LungePounce(Handle:event, const String:name[], bool:dontBroadcast)
{
	new attacker = GetClientOfUserId(GetEventInt(event, "userid"));
	new victim = GetClientOfUserId(GetEventInt(event, "victim"));
	g_bIsPouncing[attacker] = false;
	g_bHasLandedPounce[attacker] = true;

	// Don't count pounce stats for pounces on incapped survivors
	if (IsIncapped(victim)) return;

	g_iMapStats[attacker][PouncesLanded]++;
	g_iMapStats[victim][PouncesEaten]++;
	if (GetEventInt(event, "damage") >= g_iMinDPDamage)
	{
		g_iMapStats[attacker][DPsLanded]++;
		g_iMapStats[victim][DPsEaten]++;
	}
}

public Event_PlayerBoomed(Handle:event, const String:name[], bool:dontBroadcast)
{
	// This will only occur if the plugin is loaded mid map (and a boomer is already spawned)
	if (!g_iBoomerClient)
	{
		g_iBoomerClient = GetClientOfUserId(GetEventInt(event, "attacker"));
	}

	if (!g_bHasBoomLanded)
	{
		g_iMapStats[g_iBoomerClient][BoomSuccesses]++;
		g_bHasBoomLanded = true;
	}

	// Doesn't matter if we log stats to an out of play client, won't affect anything
	// if (!IsClientInGame(g_iBoomerClient) || IsFakeClient(g_iBoomerClient)) return;

	// We credit the person who spawned the boomer with booms even if it went AI
	if (GetEventBool(event, "exploded"))
	{
		// possible TODO: g_iBoomerKiller's fault, use this for something?
		g_iMapStats[g_iBoomerClient][BoomedSurvivorsByProxy]++;
	}
	else
	{
		g_iMapStats[g_iBoomerClient][BoomedSurvivorsByVomit]++;
	}
}

ClearMapStats()
{
	for (new i = 1; i <= MaxClients; i++)
	{
		for (new j = 0; j < STATS_MAX; j++) g_iMapStats[i][j] = 0;
		g_iWitchDamage[i] = 0;
		ClearDamage(i);
	}
	g_iAccumulatedWitchDamage = 0;
	ResetWitchTracking();
}

ClearPlayerStatsAndState(client)
{
	for (new i = 0; i < STATS_MAX; i++) g_iMapStats[client][i] = 0;
	g_iWitchDamage[client] = 0;
	ClearDamage(client);
}

ResetWitchTracking()
{
	for (new i = MaxClients + 1; i < MAXENTITIES; i++) g_bIsWitch[i] = false;
}

// Clear g_iDamageDealt, g_iShotsDealt, and g_iLastHealth for given client
ClearDamage(client)
{
	g_iLastHealth[client] = 0;
	for (new i = 1; i <= MaxClients; i++)
	{
		g_iDamageDealt[client][i] = 0;
		g_iShotsDealt[client][i] = 0;
	}
}

bool:IsCommonInfected(entity)
{
	if(entity && IsValidEntity(entity) && IsValidEdict(entity))
	{
		decl String:classname[32];
		GetEdictClassname(entity, classname, sizeof(classname));
		return StrEqual(classname, "infected");
	}
	return false;
}  

// Takes 2D arrays [index] = {client, value}
public ClientValue2DSortDesc(x[], y[], const array[][], Handle:data)
{
	if (x[1] > y[1]) return -1;
	else if (x[1] < y[1]) return 1;
	else return 0;
}

// Jacked from skeet announce
bool:IsGrounded(client)
{
	return (GetEntProp(client, Prop_Data, "m_fFlags") & FL_ONGROUND) > 0;
}

PrintToTeamConsole(teamflag, const String:format[], any:...)
{
	decl String:buffer[1024];

	for(new i = 1;i <= MaxClients;i++)
	{
		if(IsClientInGame(i) && (!teamflag || teamflag & (1 << GetClientTeam(i))))
		{
			VFormat(buffer, sizeof(buffer), format, 3);
			PrintToConsole(i, buffer);
		}
	}
}
ZBzibing is offline
Marttt
Veteran Member
Join Date: Jan 2019
Location: Brazil
Old 11-14-2021 , 09:16   Re: [L4D] Competitive Stats New grammar help
Reply With Quote #2

You should share the .sp file aswell, sometimes copying from the forum changes the "original" formatting.

Btw 1300 lines is too much, better ask on github repo or at the relative plugin thread.
__________________

Last edited by Marttt; 11-14-2021 at 10:06.
Marttt is offline
HarryPotter
Veteran Member
Join Date: Sep 2017
Location: Taiwan, Asia
Old 11-15-2021 , 00:54   Re: [L4D] Competitive Stats New grammar help
Reply With Quote #3

this one?
__________________
HarryPotter is offline
ZBzibing
Senior Member
Join Date: Dec 2012
Old 11-28-2021 , 03:25   Re: [L4D] Competitive Stats New grammar help
Reply With Quote #4

Quote:
Originally Posted by HarryPotter View Post
Yeah, that's it. I got a compilation error

fatal error 183: cannot read from file: "include/sdkhooks.inc"

My version is SM110-6522 MM111-1145
__________________
Please forgive, If I'm not describing it accurately. I use google translate
Functional tests are all from L4D1, and are only keen to solve and fix various bugs of L4D1:
ZBzibing is offline
Silvers
SourceMod Plugin Approver
Join Date: Aug 2010
Location: SpaceX
Old 11-28-2021 , 04:10   Re: [L4D] Competitive Stats New grammar help
Reply With Quote #5

Where are you compiling the plugin? Local sourcemod folder? If so you didn't copy the whole installation package correctly. The file "include/sdkhooks.inc" is provided with SourceMod. Even online compilers have this file since it's been with SourceMod for many versions now.
__________________

Last edited by Silvers; 11-28-2021 at 04:11.
Silvers is offline
ZBzibing
Senior Member
Join Date: Dec 2012
Old 11-28-2021 , 05:46   Re: [L4D] Competitive Stats New grammar help
Reply With Quote #6

Quote:
Originally Posted by Silvers View Post
Where are you compiling the plugin? Local sourcemod folder? If so you didn't copy the whole installation package correctly. The file "include/sdkhooks.inc" is provided with SourceMod. Even online compilers have this file since it's been with SourceMod for many versions now.
I'm local
SM1.3 can be compiled successfully
SM1.8 - SM110 failure
__________________
Please forgive, If I'm not describing it accurately. I use google translate
Functional tests are all from L4D1, and are only keen to solve and fix various bugs of L4D1:
ZBzibing is offline
Silvers
SourceMod Plugin Approver
Join Date: Aug 2010
Location: SpaceX
Old 11-28-2021 , 06:57   Re: [L4D] Competitive Stats New grammar help
Reply With Quote #7

Install sourcemod again, you didn't copy all the files.
__________________
Silvers is offline
ZBzibing
Senior Member
Join Date: Dec 2012
Old 11-28-2021 , 07:27   Re: [L4D] Competitive Stats New grammar help
Reply With Quote #8

I tried to fix it and now SM1.10 compiles
Attached Files
File Type: sp Get Plugin or Get Source (l4dcompstats.sp - 95 views - 41.4 KB)
__________________
Please forgive, If I'm not describing it accurately. I use google translate
Functional tests are all from L4D1, and are only keen to solve and fix various bugs of L4D1:
ZBzibing is offline
Reply



Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT -4. The time now is 18:12.


Powered by vBulletin®
Copyright ©2000 - 2024, vBulletin Solutions, Inc.
Theme made by Freecode