-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathl4d_tank_damage_announce.sp
More file actions
331 lines (278 loc) · 13.6 KB
/
l4d_tank_damage_announce.sp
File metadata and controls
331 lines (278 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
/*
SourcePawn is Copyright (C) 2006-2008 AlliedModders LLC. All rights reserved.
SourceMod is Copyright (C) 2006-2008 AlliedModders LLC. All rights reserved.
Pawn and SMALL are Copyright (C) 1997-2008 ITB CompuPhase.
Source is Copyright (C) Valve Corporation.
All trademarks are property of their respective owners.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma semicolon 1
#include <sourcemod>
new const TEAM_SURVIVOR = 2;
new const TEAM_INFECTED = 3;
new const ZOMBIECLASS_TANK = 8; // Zombie class of the tank, used to find tank after he have been passed to another player
new bool: g_bEnabled = true;
new bool: g_bAnnounceTankDamage = false; // Whether or not tank damage should be announced
new bool: g_bIsTankInPlay = false; // Whether or not the tank is active
new g_iOffset_Incapacitated = 0; // Used to check if tank is dying
new g_iTankClient = 0; // Which client is currently playing as tank
new g_iLastTankHealth = 0; // Used to award the killing blow the exact right amount of damage
new g_iSurvivorLimit = 4; // For survivor array in damage print
new g_iDamage[MAXPLAYERS + 1];
new Float: g_fMaxTankHealth = 6000.0;
new Handle: g_hCvarEnabled = INVALID_HANDLE;
new Handle: g_hCvarTankHealth = INVALID_HANDLE;
new Handle: g_hCvarSurvivorLimit = INVALID_HANDLE;
public Plugin:myinfo =
{
name = "Tank Damage Announce L4D2",
author = "Griffin and Blade",
description = "Announce damage dealt to tanks by survivors",
version = "0.6.5d"
};
public OnPluginStart()
{
g_bIsTankInPlay = false;
g_bAnnounceTankDamage = false;
g_iTankClient = 0;
ClearTankDamage();
HookEvent("tank_spawn", Event_TankSpawn);
HookEvent("player_death", Event_PlayerKilled);
HookEvent("round_start", Event_RoundStart);
HookEvent("round_end", Event_RoundEnd);
HookEvent("player_hurt", Event_PlayerHurt);
g_hCvarEnabled = CreateConVar("l4d_tankdamage_enabled", "1", "Announce damage done to tanks when enabled", FCVAR_PLUGIN|FCVAR_SPONLY|FCVAR_NOTIFY, true, 0.0, true, 1.0);
g_hCvarSurvivorLimit = FindConVar("survivor_limit");
g_hCvarTankHealth = FindConVar("z_tank_health");
HookConVarChange(g_hCvarEnabled, Cvar_Enabled);
HookConVarChange(g_hCvarSurvivorLimit, Cvar_SurvivorLimit);
HookConVarChange(g_hCvarTankHealth, Cvar_TankHealth);
g_bEnabled = GetConVarBool(g_hCvarEnabled);
CalculateTankHealth();
g_iOffset_Incapacitated = FindSendPropInfo("Tank", "m_isIncapacitated");
}
public OnMapStart()
{
// In cases where a tank spawns and map is changed manually, bypassing round end
ClearTankDamage();
}
public OnClientDisconnect_Post(client)
{
if (!g_bIsTankInPlay || client != g_iTankClient) return;
CreateTimer(0.1, Timer_CheckTank, client); // Use a delayed timer due to bugs where the tank passes to another player
}
public Cvar_Enabled(Handle:convar, const String:oldValue[], const String:newValue[])
{
g_bEnabled = StringToInt(newValue) > 0 ? true:false;
}
public Cvar_SurvivorLimit(Handle:convar, const String:oldValue[], const String:newValue[])
{
g_iSurvivorLimit = StringToInt(newValue);
}
public Cvar_TankHealth(Handle:convar, const String:oldValue[], const String:newValue[])
{
CalculateTankHealth();
}
CalculateTankHealth()
{
g_fMaxTankHealth = GetConVarFloat(g_hCvarTankHealth) * 1.5;
if (g_fMaxTankHealth <= 0.0) g_fMaxTankHealth = 1.0; // No dividing by 0!
}
public Event_PlayerHurt(Handle:event, const String:name[], bool:dontBroadcast)
{
if (!g_bIsTankInPlay) return; // No tank in play; no damage to record
new victim = GetClientOfUserId(GetEventInt(event, "userid"));
if (victim != GetTankClient() || // Victim isn't tank; no damage to record
IsTankDying() // Something buggy happens when tank is dying with regards to damage
) return;
new attacker = GetClientOfUserId(GetEventInt(event, "attacker"));
// We only care about damage dealt by survivors, though it can be funny to see
// claw/self inflicted hittable damage, so maybe in the future we'll do that
if (attacker == 0 || // Damage from world?
!IsClientInGame(attacker) || // Not sure if this happens
GetClientTeam(attacker) != TEAM_SURVIVOR
) return;
g_iDamage[attacker] += GetEventInt(event, "dmg_health");
g_iLastTankHealth = GetEventInt(event, "health");
}
public Event_PlayerKilled(Handle:event, const String:name[], bool:dontBroadcast)
{
if (!g_bIsTankInPlay) return; // No tank in play; no damage to record
new victim = GetClientOfUserId(GetEventInt(event, "userid"));
if (victim != g_iTankClient) return;
// Award the killing blow's damage to the attacker; we don't award
// damage from player_hurt after the tank has died/is dying
// If we don't do it this way, we get wonky/inaccurate damage values
new attacker = GetClientOfUserId(GetEventInt(event, "attacker"));
if (attacker && IsClientInGame(attacker)) g_iDamage[attacker] += g_iLastTankHealth;
// Damage announce could probably happen right here...
CreateTimer(0.1, Timer_CheckTank, victim); // Use a delayed timer due to bugs where the tank passes to another player
}
public Event_TankSpawn(Handle:event, const String:name[], bool:dontBroadcast)
{
new client = GetClientOfUserId(GetEventInt(event, "userid"));
g_iTankClient = client;
if (g_bIsTankInPlay) return; // Tank passed
// New tank, damage has not been announced
g_bAnnounceTankDamage = true;
g_bIsTankInPlay = true;
// Set health for damage print in case it doesn't get set by player_hurt (aka no one shoots the tank)
g_iLastTankHealth = GetClientHealth(client);
}
public Event_RoundStart(Handle:event, const String:name[], bool:dontBroadcast)
{
g_bIsTankInPlay = false;
g_iTankClient = 0;
ClearTankDamage(); // Probably redundant
}
// When survivors wipe or juke tank, announce damage
public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast)
{
// But only if a tank that hasn't been killed exists
if (g_bAnnounceTankDamage)
{
PrintRemainingHealth();
PrintTankDamage();
}
ClearTankDamage();
}
public Action:Timer_CheckTank(Handle:timer, any:oldtankclient)
{
if (g_iTankClient != oldtankclient) return; // Tank passed
new tankclient = FindTankClient();
if (tankclient && tankclient != oldtankclient)
{
g_iTankClient = tankclient;
return; // Found tank, done
}
if (g_bAnnounceTankDamage) PrintTankDamage();
ClearTankDamage();
g_bIsTankInPlay = false; // No tank in play
}
bool:IsTankDying()
{
new tankclient = GetTankClient();
if (!tankclient) return false;
return bool:GetEntData(tankclient, g_iOffset_Incapacitated);
}
PrintRemainingHealth()
{
if (!g_bEnabled) return;
new tankclient = GetTankClient();
if (!tankclient) return;
decl String:name[MAX_NAME_LENGTH];
if (IsFakeClient(tankclient)) name = "AI";
else GetClientName(tankclient, name, sizeof(name));
PrintToChatAll("\x01[SM] Tank (\x03%s\x01) had \x05%d\x01 health remaining", name, g_iLastTankHealth);
}
PrintTankDamage()
{
if (!g_bEnabled) return;
PrintToChatAll("[SM] Damage dealt to tank:");
new client;
new percent_total; // Accumulated total of calculated percents, for fudging out numbers at the end
new damage_total; // Accumulated total damage dealt by survivors, to see if we need to fudge upwards to 100%
new survivor_index = -1;
new survivor_clients[g_iSurvivorLimit]; // Array to store survivor client indexes in, for the display iteration
decl percent_damage, damage;
for (client = 1; client <= MaxClients; client++)
{
if (!IsClientInGame(client) || GetClientTeam(client) != TEAM_SURVIVOR) continue;
survivor_index++;
survivor_clients[survivor_index] = client;
damage = g_iDamage[client];
damage_total += damage;
percent_damage = GetDamageAsPercent(damage);
percent_total += percent_damage;
}
SortCustom1D(survivor_clients, g_iSurvivorLimit, SortByDamageDesc);
new percent_adjustment;
// Percents add up to less than 100% AND > 99.5% damage was dealt to tank
if ((percent_total < 100 &&
float(damage_total) > (g_fMaxTankHealth - (g_fMaxTankHealth / 200.0)))
)
{
percent_adjustment = 100 - percent_total;
}
new last_percent = 100; // Used to store the last percent in iteration to make sure an adjusted percent doesn't exceed the previous percent
decl adjusted_percent_damage;
for (new i; i <= survivor_index; i++)
{
client = survivor_clients[i];
damage = g_iDamage[client];
percent_damage = GetDamageAsPercent(damage);
// Attempt to adjust the top damager's percent, defer adjustment to next player if it's an exact percent
// e.g. 3000 damage on 6k health tank shouldn't be adjusted
if (percent_adjustment != 0 && // Is there percent to adjust
damage > 0 && // Is damage dealt > 0%
!IsExactPercent(damage) // Percent representation is not exact, e.g. 3000 damage on 6k tank = 50%
)
{
adjusted_percent_damage = percent_damage + percent_adjustment;
if (adjusted_percent_damage <= last_percent) // Make sure adjusted percent is not higher than previous percent, order must be maintained
{
percent_damage = adjusted_percent_damage;
percent_adjustment = 0;
}
}
last_percent = percent_damage;
PrintToChatAll("\x05%4d\x01 [\x04%d%%\x01]: \x03%N\x01", damage, percent_damage, client);
}
}
ClearTankDamage()
{
g_iLastTankHealth = 0;
for (new i = 1; i <= MaxClients; i++) { g_iDamage[i] = 0; }
g_bAnnounceTankDamage = false;
}
GetTankClient()
{
if (!g_bIsTankInPlay) return 0;
new tankclient = g_iTankClient;
if (!IsClientInGame(tankclient)) // If tank somehow is no longer in the game (kicked, hence events didn't fire)
{
tankclient = FindTankClient(); // find the tank client
if (!tankclient) return 0;
g_iTankClient = tankclient;
}
return tankclient;
}
FindTankClient()
{
for (new client = 1; client <= MaxClients; client++)
{
if (!IsClientInGame(client) ||
GetClientTeam(client) != TEAM_INFECTED ||
!IsPlayerAlive(client) ||
GetEntProp(client, Prop_Send, "m_zombieClass") != ZOMBIECLASS_TANK)
continue;
return client; // Found tank, return
}
return 0;
}
GetDamageAsPercent(damage)
{
return RoundToFloor(FloatMul(FloatDiv(float(damage), g_fMaxTankHealth), 100.0));
}
bool:IsExactPercent(damage)
{
return (FloatAbs(float(GetDamageAsPercent(damage)) - FloatMul(FloatDiv(float(damage), g_fMaxTankHealth), 100.0)) < 0.001) ? true:false;
}
public SortByDamageDesc(elem1, elem2, const array[], Handle:hndl)
{
// By damage, then by client index, descending
if (g_iDamage[elem1] > g_iDamage[elem2]) return -1;
else if (g_iDamage[elem2] > g_iDamage[elem1]) return 1;
else if (elem1 > elem2) return -1;
else if (elem2 > elem1) return 1;
return 0;
}