· 5 years ago · Jun 01, 2020, 01:24 AM
1/*
2 * ============================================================================
3 *
4 * Zombie:Reloaded
5 *
6 * File: infect.inc
7 * Type: Core
8 * Description: Client infection functions.
9 *
10 * Copyright (C) 2009-2013 Greyscale, Richard Helgeby
11 *
12 * This program is free software: you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation, either version 3 of the License, or
15 * (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 *
25 * ============================================================================
26 */
27
28/**
29 * @section Explosion flags.
30 */
31#define EXP_NODAMAGE 1
32#define EXP_REPEATABLE 2
33#define EXP_NOFIREBALL 4
34#define EXP_NOSMOKE 8
35#define EXP_NODECAL 16
36#define EXP_NOSPARKS 32
37#define EXP_NOSOUND 64
38#define EXP_RANDOMORIENTATION 128
39#define EXP_NOFIREBALLSMOKE 256
40#define EXP_NOPARTICLES 512
41#define EXP_NODLIGHTS 1024
42#define EXP_NOCLAMPMIN 2048
43#define EXP_NOCLAMPMAX 4096
44/**
45 * @endsection
46 */
47
48/**
49 * @section Other defines. (D47: zr_infect_event_weapon)
50 */
51#define MAX_EVENT_LENGTH 32
52 /**
53 * @endsection
54 */
55
56/**
57 * @section Global variables to store infect timer handles.
58 */
59new Handle:tInfect = INVALID_HANDLE;
60new Handle:tInfectCountdown = INVALID_HANDLE;
61/**
62 * @endsection
63 */
64
65/**
66 * Infection countdown data pack.
67 */
68new Handle:hInfectCountdownData = INVALID_HANDLE;
69
70/**
71 * Array for flagging client as zombie.
72 */
73new bool:bZombie[MAXPLAYERS + 1];
74
75/**
76 * @section bInfectImmune indexes
77 */
78#define INFECT_TYPE_MOTHER 0
79#define INFECT_TYPE_NORMAL 1
80/**
81 * @endsection
82 */
83
84/**
85 * Array for flagging client to be protected. (See defines above)
86 */
87new bool:bInfectImmune[MAXPLAYERS + 1][2];
88
89/**
90 * Available mother zombie infection modes.
91 */
92enum InfectMode
93{
94 InfectMode_Invalid = -1, /** Invalid mode, used by validators. */
95 InfectMode_Dynamic, /** Every n-th player is infected. */
96 InfectMode_Absolute, /** Keep n humans (negative n) or infect n zombies. */
97 InfectMode_Range /** An absolute number of zombies infected (min to max). */
98}
99
100/**
101 * Map is ending.
102 */
103InfectOnMapEnd()
104{
105 // Reset timers. Infect timers are invalidated on a map change if they are
106 // still running.
107 ZREndTimer(tInfect);
108 ZREndTimer(tInfectCountdown);
109 InfectStopCountdown();
110}
111
112/**
113 * Loads downloadable content data for infect module.
114 */
115InfectLoad()
116{
117 // Get infection sound.
118 decl String:sound[PLATFORM_MAX_PATH];
119 GetConVarString(g_hCvarsList[CVAR_INFECT_SOUND], sound, sizeof(sound));
120
121 // If infect sound cvar is empty, then stop.
122 if (!sound[0])
123 {
124 return;
125 }
126
127 // Prepend sound/ to the path.
128 Format(sound, sizeof(sound), "sound/%s", sound);
129
130 // Add sound file to downloads table.
131 AddFileToDownloadsTable(sound);
132}
133
134/**
135 * Create commands specific to infect here.
136 */
137InfectOnCommandsCreate()
138{
139 RegConsoleCmd("zr_infect", InfectInfectCommand, "Infect a client. Usage: zr_infect <filter> [respawn - 1/0]");
140 RegConsoleCmd("zr_human", InfectHumanCommand, "Turn a client into a human. Usage: zr_human <filter> [respawn - 1/0]");
141}
142
143/**
144 * Client is joining the server.
145 *
146 * @param client The client index.
147 */
148InfectClientInit(client)
149{
150 // Reset infect immunity flags.
151 bInfectImmune[client][INFECT_TYPE_MOTHER] = false;
152 bInfectImmune[client][INFECT_TYPE_NORMAL] = false;
153}
154
155/**
156 * Client is leaving the server.
157 *
158 * @param client The client index.
159 */
160InfectOnClientDisconnect(client)
161{
162 // If client is still connecting, then stop.
163 if (!IsClientInGame(client))
164 {
165 return;
166 }
167
168 // If zombie hasn't spawned, then stop.
169 if (!g_bZombieSpawned)
170 {
171 return;
172 }
173
174 // If client is dead, then stop.
175 if (!IsPlayerAlive(client))
176 {
177 return;
178 }
179
180 // Initialize count variables
181 new zombiecount;
182 new humancount;
183
184 // Count valid clients.
185 ZRCountValidClients(zombiecount, humancount);
186
187 // If client is a human.
188 if (InfectIsClientHuman(client))
189 {
190 // If there are other humans (ignoring this human), then stop.
191 if (humancount > 1)
192 {
193 return;
194 }
195
196 // If there are no more clients in the server, then stop.
197 if (!ZRTeamHasClients(CS_TEAM_T))
198 {
199 return;
200 }
201
202 // Manually terminate round.
203 RoundEndTerminateRound(ROUNDEND_DELAY, ZombiesWin);
204
205 return;
206 }
207
208 // We know here that player is a zombie.
209
210 // If there is 1 or less humans, then stop.
211 if (humancount <= 1)
212 {
213 return;
214 }
215
216 // If there are other zombies (ignoring this zombie), then stop.
217 if (zombiecount - 1)
218 {
219 return;
220 }
221
222 // Create eligible player list.
223 new Handle:arrayEligibleClients = INVALID_HANDLE;
224
225 // Create eligible client list, with no mother infect immunities
226 new eligibleclients = ZRCreateEligibleClientList(arrayEligibleClients, true, true, true);
227
228 // If there are no eligible client's then stop.
229 if (!eligibleclients)
230 {
231 // Destroy handle.
232 CloseHandle(arrayEligibleClients);
233 return;
234 }
235
236 // Get a random valid array index.
237 new randindex = Math_GetRandomInt(0, eligibleclients - 1);
238
239 // Get the client stored in the random array index.
240 new randclient = GetArrayCell(arrayEligibleClients, randindex);
241
242 // Infect player.
243 InfectHumanToZombie(randclient);
244
245 // Tell client they have been randomly been chosen to replace disconnecting zombie.
246 TranslationPrintToChat(randclient, "Infect disconnect");
247
248 // Destroy handle.
249 CloseHandle(arrayEligibleClients);
250}
251
252/**
253 * Client is joining a team.
254 *
255 * @param client The client index.
256 * @param team The team index.
257 */
258InfectOnClientTeam(client, team)
259{
260 // If client isn't joining spec, then stop.
261 if (team != CS_TEAM_SPECTATOR)
262 {
263 return;
264 }
265
266 // Disable zombie flag on client.
267 bZombie[client] = false;
268}
269
270/**
271 * Client is spawning into the game.
272 *
273 * @param client The client index.
274 */
275InfectOnClientSpawn(client)
276{
277 // Disable zombie flag on client.
278 bZombie[client] = false;
279
280 // Check if client is spawning on the terrorist team.
281 if (ZRIsClientOnTeam(client, CS_TEAM_T))
282 {
283 if (g_bZombieSpawned)
284 {
285 CS_SwitchTeam(client, CS_TEAM_CT);
286 CS_RespawnPlayer(client);
287 }
288 }
289}
290
291/**
292 * Client has been killed.
293 *
294 * @param client The client index.
295 * @param attacker The attacker index.
296 */
297InfectOnClientDeath(client, attacker)
298{
299 // If attacker isn't valid, then stop.
300 if (!ZRIsClientValid(attacker))
301 {
302 return;
303 }
304
305 // If attacker isn't a human, then stop.
306 if (!InfectIsClientHuman(attacker))
307 {
308 return;
309 }
310
311 // If client isn't a zombie, then stop.
312 if (!InfectIsClientInfected(client))
313 {
314 return;
315 }
316
317 // Add kill bonus to attacker's score.
318 new bonus = ClassGetKillBonus(client);
319 new score = ToolsClientScore(attacker, true, false);
320 ToolsClientScore(attacker, true, true, score + bonus);
321}
322
323/**
324 * Client has been hurt.
325 *
326 * @param client The client index.
327 * @param attacker The attacker index.
328 * @param weapon The weapon used.
329 */
330InfectOnClientHurt(client, attacker, const String:weapon[])
331{
332 // If attacker isn't valid, then stop.
333 if (!ZRIsClientValid(attacker))
334 {
335 return;
336 }
337
338 // If client isn't a human, then stop.
339 if (!InfectIsClientHuman(client))
340 {
341 return;
342 }
343
344 // Attacker isn't a zombie, then stop.
345 if (!InfectIsClientInfected(attacker))
346 {
347 return;
348 }
349
350 // If client has infect immunity, then stop.
351 if (bInfectImmune[client][INFECT_TYPE_NORMAL])
352 {
353 return;
354 }
355
356 // If weapon isn't a knife, then stop.
357 if (!StrEqual(weapon, "knife") && !StrEqual(weapon, "bayonet"))
358 {
359 return;
360 }
361
362 // Check if the immunity module is handling the infection.
363 if (ImmunityOnClientInfect(client, attacker))
364 {
365 //PrintToChatAll("InfectOnClientHurt - Infect blocked.");
366 return;
367 }
368
369 // Infect client.
370 InfectHumanToZombie(client, attacker);
371}
372
373/**
374 * The round is starting.
375 */
376InfectOnRoundStart()
377{
378 // Stop infect timers if running.
379 ZREndTimer(tInfect);
380 ZREndTimer(tInfectCountdown);
381
382 // Tell plugin there are no zombies.
383 g_bZombieSpawned = false;
384}
385
386/**
387 * The freeze time is ending.
388 */
389InfectOnRoundFreezeEnd()
390{
391 // Stop infect timers if running.
392 ZREndTimer(tInfect);
393 ZREndTimer(tInfectCountdown);
394
395 // If the zombie has spawned already (had to be through admin) then stop.
396 if (g_bZombieSpawned)
397 {
398 return;
399 }
400
401 // Get min and max times.
402 new Float:infectspawntimemin = GetConVarFloat(g_hCvarsList[CVAR_INFECT_SPAWNTIME_MIN]);
403 new Float:infectspawntimemax = GetConVarFloat(g_hCvarsList[CVAR_INFECT_SPAWNTIME_MAX]);
404
405 // Pick random time between min and max.
406 new Float:randomtime = GetRandomFloat(infectspawntimemin, infectspawntimemax);
407
408 // Round to the nearest whole number (and convert back to a float) so the countdown is synched with it.
409 float(RoundToNearest(randomtime));
410
411 tInfect = CreateTimer(randomtime, InfectMotherZombie, _, TIMER_FLAG_NO_MAPCHANGE);
412
413 // Check cvar and start a countdown timer if enabled.
414 new bool:countdown = GetConVarBool(g_hCvarsList[CVAR_INFECT_MZOMBIE_COUNTDOWN]);
415 if (countdown && randomtime > 1.0)
416 {
417 // Stop old countdown timer, if it exists.
418 InfectStopCountdown();
419
420 // Store the time until infection, and initialize the counter.
421 hInfectCountdownData = CreateDataPack();
422 WritePackFloat(hInfectCountdownData, randomtime);
423 WritePackFloat(hInfectCountdownData, 0.0);
424 tInfectCountdown = CreateTimer(1.0, InfectCountdown, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
425
426 // Display initial tick.
427 InfectCountdown(tInfectCountdown);
428 }
429}
430
431/**
432 * The round is ending.
433 */
434InfectOnRoundEnd()
435{
436 // Stop infect timers if running.
437 ZREndTimer(tInfect);
438 ZREndTimer(tInfectCountdown);
439
440 // x = client index.
441 for (new x = 1; x <= MaxClients; x++)
442 {
443 // If client isn't in-game, then stop.
444 if (!IsClientInGame(x))
445 {
446 continue;
447 }
448
449 // Disable zombie flag on client.
450 bZombie[x] = false;
451 }
452}
453
454/**
455 * Timer callback, chooses mother zombies.
456 *
457 * @param timer The timer handle.
458 */
459public Action:InfectMotherZombie(Handle:timer)
460{
461 // Reset timer handle.
462 tInfect = INVALID_HANDLE;
463
464 // Create eligible player list.
465 new Handle:arrayEligibleClients = INVALID_HANDLE;
466 new eligibleclients = ZRCreateEligibleClientList(arrayEligibleClients, true, true, true);
467
468 // If there are no eligible client's then stop.
469 if (!eligibleclients)
470 {
471 // Destroy handle.
472 CloseHandle(arrayEligibleClients);
473 return;
474 }
475
476 // Prune list of immune clients.
477 eligibleclients = InfectRemoveImmuneClients(arrayEligibleClients);
478
479 // Move all clients to CT.
480 InfectMoveAllToCT();
481
482 new mothercount;
483 new ratio = GetConVarInt(g_hCvarsList[CVAR_INFECT_MZOMBIE_RATIO]);
484 new min = GetConVarInt(g_hCvarsList[CVAR_INFECT_MZOMBIE_MIN]);
485 new max = GetConVarInt(g_hCvarsList[CVAR_INFECT_MZOMBIE_MAX]);
486
487 // Count valid human clients.
488 new humancount;
489 ZRCountValidClients(_, humancount, _, true);
490
491 // Get and validate infection mode. This will also log a warning on error.
492 new InfectMode:mode = InfectGetModeOrFail();
493
494 // Apply infection mode.
495 switch (mode)
496 {
497 case InfectMode_Invalid:
498 {
499 // Validation failed. Fall back to one mother zombie.
500 mothercount = 1;
501 }
502 case InfectMode_Dynamic:
503 {
504 // Dynamic mode. Every n-th player is infected.
505
506 // A ratio of 0 will infect one zombie (to keep backwards compatibility).
507 if (ratio == 0)
508 {
509 mothercount = 1;
510 }
511
512 // Calculate number of zombies to infect.
513 mothercount = RoundToNearest(float(humancount) / ratio);
514
515 // Require at least one mother zombie.
516 if (mothercount == 0)
517 {
518 mothercount = 1;
519 }
520 }
521 case InfectMode_Absolute:
522 {
523 if (ratio > 0)
524 {
525 // Infect n humans.
526 mothercount = ratio;
527 }
528 else
529 {
530 // Infect all but n humans. Since ratio already is negative
531 // just add the numbers. (Zero ratio is catched by validator.)
532 mothercount = humancount + ratio;
533
534 // Force at least one mother zombie.
535 if (mothercount == 0)
536 {
537 mothercount = 1;
538 }
539 }
540 }
541 case InfectMode_Range:
542 {
543 // Get a random number between the range.
544 mothercount = Math_GetRandomInt(min, max);
545 }
546 }
547
548 // Infect players.
549 for (new n = 0; n < mothercount; n++)
550 {
551 // Recount eligible clients.
552 eligibleclients = GetArraySize(arrayEligibleClients);
553
554 // Stop if there are no more eligible clients.
555 if (eligibleclients <= 0)
556 {
557 break;
558 }
559
560 // Get a random array index.
561 new i = Math_GetRandomInt(0, eligibleclients - 1);
562
563 // Get the client stored in the random array index.
564 new client = GetArrayCell(arrayEligibleClients, i);
565
566 // Infect player.
567 InfectHumanToZombie(client, _, true);
568
569 // Remove player from eligible client list.
570 RemoveFromArray(arrayEligibleClients, i);
571 }
572
573 // Mother zombies have been infected.
574 g_bZombieSpawned = true;
575
576 // Destroy client list.
577 CloseHandle(arrayEligibleClients);
578}
579
580/**
581 * Moves all alive clients to the CT team.
582 */
583InfectMoveAllToCT()
584{
585 // Move all clients to CT
586 for (new client = 1; client <= MaxClients; client++)
587 {
588 // If client isn't in-game, then stop.
589 if (!IsClientInGame(client))
590 {
591 continue;
592 }
593
594 // If client is dead, then stop.
595 if (!IsPlayerAlive(client))
596 {
597 continue;
598 }
599
600 // Switch client to CT team.
601 CS_SwitchTeam(client, CS_TEAM_CT);
602 }
603}
604
605/**
606 * Removes immune clients from a client list. If a client is removed, their
607 * immunity is also removed.
608 *
609 * @param clientList List of clients.
610 * @param keepLastPlayer Don't remove if there's only one player left.
611 *
612 * @return Number of clients remaining.
613 */
614InfectRemoveImmuneClients(Handle:clientList, bool:keepLastPlayer = true)
615{
616 new len = GetArraySize(clientList);
617
618 // Loop though client list.
619 for (new i = 0; i < len; i++)
620 {
621 // Stop pruning if there is only one player left.
622 if (keepLastPlayer && len <= 1)
623 {
624 break;
625 }
626
627 // Get client.
628 new client = GetArrayCell(clientList, i);
629
630 // Check if client is immune from mother zombie infection.
631 if (bInfectImmune[client][INFECT_TYPE_MOTHER])
632 {
633 // Take away immunity.
634 bInfectImmune[client][INFECT_TYPE_MOTHER] = false;
635
636 // Remove client from array.
637 RemoveFromArray(clientList, i);
638
639 // Update list size.
640 len--;
641
642 // Backtrack one index, because we deleted it out from under the loop.
643 i--;
644 }
645 }
646
647 return len;
648}
649
650/**
651 * Timer callback, displays countdown to clients.
652 *
653 * @param timer The timer handle.
654 */
655public Action:InfectCountdown(Handle:timer)
656{
657 new bool:countdown = GetConVarBool(g_hCvarsList[CVAR_INFECT_MZOMBIE_COUNTDOWN]);
658 if (!countdown)
659 {
660 InfectStopCountdown();
661 return Plugin_Stop;
662 }
663
664 // Read the info from the datapack.
665 ResetPack(hInfectCountdownData);
666 new Float:length = ReadPackFloat(hInfectCountdownData);
667 new Float:counter = ReadPackFloat(hInfectCountdownData);
668
669 // Check if the countdown has finished.
670 if (counter >= length)
671 {
672 InfectStopCountdown();
673 return Plugin_Stop;
674 }
675
676 new counter_int = RoundToNearest(length - counter);
677 char sounddir[128];
678 Format(sounddir, 128, "sound/zm/countdown/%i.mp3", counter_int);
679
680 if(FileExists(sounddir))
681 {
682 ReplaceString(sounddir, 128, "sound/", "");
683
684 if(g_Game == Game_CSGO) EmitSoundToAllAny(sounddir);
685 else EmitSoundToAll(sounddir);
686 }
687
688 // Print the countdown text to the clients.
689 TranslationPrintCenterTextAll(false, "Infect countdown", counter_int);
690
691 counter++;
692
693 // Write the new counter value to the datapack.
694 ResetPack(hInfectCountdownData);
695 WritePackFloat(hInfectCountdownData, length);
696 WritePackFloat(hInfectCountdownData, counter);
697
698 return Plugin_Continue;
699}
700
701CountDown()
702{
703 int count = 1;
704 char sounddir[128];
705 Format(sounddir, 128, "sound/zm/countdown/%i.mp3", count);
706
707 while(FileExists(sounddir))
708 {
709 AddFileToDownloadsTable(sounddir);
710 ReplaceString(sounddir, 128, "sound/", "");
711
712 if(g_Game != Game_CSGO) PrecacheSound(sounddir);
713 else PrecacheSoundAny(sounddir);
714
715 count++;
716 Format(sounddir, 128, "sound/zm/countdown/%i.mp3", count);
717 }
718}
719
720/**
721 * Stops the infection countdown timer.
722 */
723InfectStopCountdown()
724{
725 // Kill the timer.
726 ZREndTimer(tInfectCountdown);
727
728 // Destroy data pack.
729 if (hInfectCountdownData != INVALID_HANDLE)
730 {
731 CloseHandle(hInfectCountdownData);
732 hInfectCountdownData = INVALID_HANDLE;
733 }
734}
735
736/**
737 * Infects a client. Execute events, sets attributes and flags that indicate
738 * that the client is a zombie.
739 *
740 * @param client The client to infect.
741 * @param attacker (Optional) The attacker who did the infect.
742 * @param motherinfect (Optional) Indicates a mother zombie infect.
743 * @param respawnoverride (Optional) Set to true to override respawn cvar.
744 * @param respawn (Optional) Value to override with.
745 */
746InfectHumanToZombie(client, attacker = -1, bool:motherinfect = false, bool:respawnoverride = false, bool:respawn = false)
747{
748 // Forward pre-event to modules.
749 new Action:result = APIOnClientInfect(client, attacker, motherinfect, respawnoverride, respawn);
750
751 // Check if infection should be blocked.
752 if (result == Plugin_Handled)
753 {
754 return;
755 }
756
757 // Mark player as zombie.
758 bZombie[client] = true;
759
760 // Check if consecutive infection protection is enabled.
761 new bool:infectconsecutiveblock = GetConVarBool(g_hCvarsList[CVAR_INFECT_CONSECUTIVE_BLOCK]);
762 if (infectconsecutiveblock)
763 {
764 // If this is a mother infect, flag the player as immune for next mother
765 // infection. Otherwise do nothing and keep the current flag.
766 if (motherinfect)
767 {
768 bInfectImmune[client][INFECT_TYPE_MOTHER] = true;
769 }
770 }
771 else
772 {
773 // Consecutive infection protection is disabled. No immunity.
774 bInfectImmune[client][INFECT_TYPE_MOTHER] = false;
775 }
776
777 // Apply effects.
778 InfectFireEffects(client);
779
780 // Stop coundown, if running.
781 InfectStopCountdown();
782
783 // If attacker is valid, then continue.
784 if (ZRIsClientValid(attacker))
785 {
786 // Create and send custom player_death event.
787 new Handle:event = CreateEvent("player_death");
788 if (event != INVALID_HANDLE)
789 {
790 decl String:eventWeapon[MAX_EVENT_LENGTH];
791 GetConVarString(g_hCvarsList[CVAR_INFECT_EVENT_WEAPON], eventWeapon, MAX_EVENT_LENGTH);
792 SetEventInt(event, "userid", GetClientUserId(client));
793 SetEventInt(event, "attacker", GetClientUserId(attacker));
794 SetEventString(event, "weapon", eventWeapon);
795 FireEvent(event, false);
796 }
797
798 // Apply score and health gain.
799 InfectUpdateScore(attacker, client);
800 }
801
802 // Get a list of all client's weapon indexes.
803 new weapons[WeaponsSlot];
804 WeaponsGetClientWeapons(client, weapons);
805
806 // Check if weapons drop is enabled.
807 new bool:weaponsdrop = GetConVarBool(g_hCvarsList[CVAR_INFECT_WEAPONS_DROP]);
808
809 // This must be after the event forwarding because it fixes a problem caused by changing models in ClassOnClientInfected.
810 // Remove all weapons but knife.
811 WeaponsRemoveAllClientWeapons(client, weaponsdrop);
812
813 // Switch the player to terrorists.
814 // TODO: A solution to stop confusing bots? Respawn and teleport?
815 CS_SwitchTeam(client, CS_TEAM_T);
816
817 // If respawn is enabled, then teleport mother zombie back to spawnpoint.
818 if (motherinfect)
819 {
820 new bool:zombierespawn = GetConVarBool(g_hCvarsList[CVAR_INFECT_MZOMBIE_RESPAWN]);
821 if(zombierespawn)
822 {
823 ZTeleTeleportClient(client);
824 }
825 }
826 // Check override.
827 else
828 {
829 if (respawnoverride && respawn)
830 {
831 ZTeleTeleportClient(client);
832 }
833 }
834
835 // Print message to client.
836 // TranslationPrintToChat(client, "Infect infected");
837
838 // Forward event to modules.
839 ClassOnClientInfected(client, motherinfect);
840 RoundEndOnClientInfected();
841 DamageOnClientInfected(client, motherinfect);
842 SEffectsOnClientInfected(client);
843 ZTeleOnClientInfected(client);
844 ZHPOnClientInfected(client);
845 APIOnClientInfected(client, attacker, motherinfect, respawnoverride, respawn);
846 ImmunityOnClientInfected(client);
847}
848
849/**
850 * Turns a zombie back into a human. Execute events, sets attributes and flags that indicate
851 * that the client is a human.
852 *
853 * @param client The client to make human.
854 * @param respawn Teleport client back to spawn if true.
855 * @param protect Start spawn protection on new human.
856 */
857InfectZombieToHuman(client, bool:respawn = false, bool:protect = false)
858{
859 // Forward pre-event to modules.
860 new Action:result = APIOnClientHuman(client, respawn, protect);
861
862 // Check if action should be blocked.
863 if (result == Plugin_Handled)
864 {
865 return;
866 }
867
868 // Mark player as human.
869 bZombie[client] = false;
870
871 // Switch the player to counter-terrorists.
872 CS_SwitchTeam(client, CS_TEAM_CT);
873
874 // Set client as translation target.
875 SetGlobalTransTarget(client);
876
877 // Print message to client.
878 TranslationPrintToChat(client, "Infect human");
879
880 // Forward event to modules.
881 ClassReloadPlayer(client);
882 RoundEndOnClientInfected();
883 ZTeleOnClientInfected(client);
884
885 // Give human a new knife. (If you leave the old one there will be glitches with the knife positioning)
886 new knife = GetPlayerWeaponSlot(client, _:Slot_Melee);
887 if (knife != -1)
888 {
889 RemovePlayerItem(client, knife);
890 AcceptEntityInput(knife, "Kill");
891 GivePlayerItem(client, "weapon_knife");
892 }
893
894 // Check if we should respawn the client.
895 if (respawn)
896 {
897 ZTeleTeleportClient(client);
898 }
899
900 // Check if we should spawn protect the client.
901 if (protect)
902 {
903 SpawnProtectStart(client);
904 }
905
906 // Forward event to modules.
907 SEffectsOnClientHuman(client);
908 APIOnClientHumanPost(client, respawn, protect);
909 ImmunityOnClientHuman(client);
910}
911
912/**
913 * Updates score for attacker and victim. Applies health gain for attacker.
914 */
915InfectUpdateScore(attacker, victim)
916{
917 // Give client's infector a point.
918 new score = ToolsClientScore(attacker, true, false);
919 ToolsClientScore(attacker, true, true, ++score);
920
921 // Add a death to the zombie's score.
922 new deaths = ToolsClientScore(victim, false, false);
923 ToolsClientScore(victim, false, true, ++deaths);
924
925 // Apply infect HP gain.
926 new healthgain = ClassGetHealthInfectGain(attacker);
927 new health = GetClientHealth(attacker);
928
929 // Set attacker's new health.
930 SetEntityHealth(attacker, health + healthgain);
931
932 // Forward event to modules.
933 ZHPOnHealthInfectGain(attacker);
934}
935
936/**
937 * Creates effects on a newly infected client.
938 *
939 * @param client The client index.
940 */
941InfectFireEffects(client)
942{
943 // Initialize vector variables.
944 new Float:clientloc[3];
945 new Float:direction[3] = {0.0, 0.0, 0.0};
946
947 // Get client's position.
948 GetClientAbsOrigin(client, clientloc);
949 clientloc[2] += 30;
950
951 new bool:explosion = GetConVarBool(g_hCvarsList[CVAR_INFECT_EXPLOSION]);
952 if (explosion)
953 {
954 // Initialize explosion flags variable.
955 new flags;
956
957 // Set "nofireball" flag if fireball is disabled.
958 new bool:fireball = GetConVarBool(g_hCvarsList[CVAR_INFECT_FIREBALL]);
959 if (!fireball)
960 {
961 flags = flags | EXP_NOFIREBALL;
962 }
963
964 // Set "nosmoke" flag if smoke is disabled.
965 new bool:smoke = GetConVarBool(g_hCvarsList[CVAR_INFECT_SMOKE]);
966 if (!smoke)
967 {
968 flags = flags | EXP_NOSMOKE;
969 }
970
971 // Set "nosparks" flag if sparks are disabled.
972 new bool:sparks = GetConVarBool(g_hCvarsList[CVAR_INFECT_SPARKS]);
973 if (!sparks)
974 {
975 flags = flags | EXP_NOSPARKS;
976 }
977
978 // Create explosion at client's origin.
979 VEffectsCreateExplosion(clientloc, flags);
980 }
981
982 // Emit scream sound if enabled.
983 ZombieSoundsScream(client);
984
985 // If energy splash effect is enabled, then continue.
986 new bool:esplash = GetConVarBool(g_hCvarsList[CVAR_INFECT_ESPLASH]);
987 if (esplash)
988 {
989 // Create energy splash effect.
990 VEffectsCreateEnergySplash(clientloc, direction, true);
991 }
992
993 // If shake effect is enabled, then continue.
994 new bool:shake = GetConVarBool(g_hCvarsList[CVAR_INFECT_SHAKE]);
995 if (shake)
996 {
997 // Get shake info.
998 new Float:shakeamp = GetConVarFloat(g_hCvarsList[CVAR_INFECT_SHAKE_AMP]);
999 new Float:shakefrequency = GetConVarFloat(g_hCvarsList[CVAR_INFECT_SHAKE_FREQUENCY]);
1000 new Float:shakeduration = GetConVarFloat(g_hCvarsList[CVAR_INFECT_SHAKE_DURATION]);
1001
1002 // Shake client's screen.
1003 VEffectsShakeClientScreen(client, shakeamp, shakefrequency, shakeduration);
1004 }
1005}
1006
1007/**
1008 * Sends list of clients to infect/human.
1009 *
1010 * @param client The client index.
1011 */
1012InfectMenuClients(client)
1013{
1014 // Create menu handle.
1015 new Handle:menu_infect_clients = CreateMenu(InfectMenuClientsHandle);
1016
1017 // Set client as translation target.
1018 SetGlobalTransTarget(client);
1019
1020 decl String:title[MENU_LINE_TITLE_LENGTH];
1021 decl String:clientoption[MENU_LINE_REG_LENGTH];
1022 decl String:clientuserid[8];
1023
1024 // x = Client index.
1025 for (new x = 1; x <= MaxClients; x++)
1026 {
1027 // If client isn't in-game, then stop.
1028 if (!IsClientInGame(x))
1029 {
1030 continue;
1031 }
1032
1033 // If client isn't alive, then stop.
1034 if (!IsPlayerAlive(x))
1035 {
1036 continue;
1037 }
1038
1039 // Get client info.
1040 GetClientName(x, clientoption, sizeof(clientoption));
1041 IntToString(GetClientUserId(x), clientuserid, sizeof(clientuserid));
1042
1043 // Append client's current team to the option.
1044 if (InfectIsClientInfected(x))
1045 {
1046 Format(clientoption, sizeof(clientoption), "%s [%t]", clientoption, "Zombie");
1047 }
1048 else
1049 {
1050 Format(clientoption, sizeof(clientoption), "%s [%t]", clientoption, "Human");
1051 }
1052
1053 // Add option to menu.
1054 AddMenuItem(menu_infect_clients, clientuserid, clientoption);
1055 }
1056
1057 Format(title, sizeof(title), "%t\n ", "Infect menu clients title");
1058 SetMenuTitle(menu_infect_clients, title);
1059
1060 // Create a "Back" button to the main admin menu.
1061 SetMenuExitBackButton(menu_infect_clients, true);
1062
1063 // Send menu.
1064 DisplayMenu(menu_infect_clients, client, MENU_TIME_FOREVER);
1065}
1066
1067/**
1068 * Called when client selects option in the infect clients menu, and handles it.
1069 * @param menu_infect_clients Handle of the menu being used.
1070 * @param action The action done on the menu (see menus.inc, enum MenuAction).
1071 * @param client The client index.
1072 * @param slot The slot index selected (starting from 0).
1073 */
1074public InfectMenuClientsHandle(Handle:menu_infect_clients, MenuAction:action, client, slot)
1075{
1076 // Client selected an option.
1077 if (action == MenuAction_Select)
1078 {
1079 // Get selected client index.
1080 new target = MenuGetClientIndex(menu_infect_clients, slot);
1081
1082 // If target has left the server, then stop.
1083 if (!target)
1084 {
1085 // Re-send menu.
1086 InfectMenuClients(client);
1087 return;
1088 }
1089
1090 // Create an array with a single slot and set target to it.
1091 new targets[1];
1092 targets[0] = target;
1093
1094 // Toggle infect on the client.
1095 if (InfectIsClientInfected(target))
1096 {
1097 InfectManualHuman(client, targets, 1);
1098 }
1099 else
1100 {
1101 InfectManualInfect(client, targets, 1);
1102 }
1103
1104 // Re-send menu.
1105 InfectMenuClients(client);
1106 }
1107 // Client closed the menu.
1108 if (action == MenuAction_Cancel)
1109 {
1110 // Client hit "Back" button.
1111 if (slot == MenuCancel_ExitBack)
1112 {
1113 // Re-open admin menu.
1114 ZAdminMenu(client);
1115 }
1116 }
1117 // Client hit "Exit" button.
1118 else if (action == MenuAction_End)
1119 {
1120 CloseHandle(menu_infect_clients);
1121 }
1122}
1123
1124/**
1125 * Returns if a client is infected.
1126 *
1127 * @param client The client index.
1128 * @return True if the client has been infected, false otherwise.
1129 */
1130bool:InfectIsClientInfected(client)
1131{
1132 // If client is invalid, then stop.
1133 if (!ZRIsClientValid(client))
1134 {
1135 return false;
1136 }
1137
1138 // Return client's zombie flag.
1139 return bZombie[client];
1140}
1141
1142/**
1143 * Returns if a client is a human.
1144 *
1145 * @param client The client index.
1146 * @return True if the client is a human, false otherwise.
1147 */
1148bool:InfectIsClientHuman(client)
1149{
1150 // If client is invalid, then stop.
1151 if (!ZRIsClientValid(client))
1152 {
1153 return true;
1154 }
1155
1156 // Return opposite of client's zombie flag.
1157 return !bZombie[client];
1158}
1159
1160/**
1161 * Infecting a client manually (via zr_infect or the "Zombie Management" menu)
1162 *
1163 * @param client The client index infecting another client.
1164 * @param targets Array containing all clients to infect.
1165 * @param count The number of clients in the array.
1166 * @param respawnoverride (Optional) True to override respawn cvar.
1167 * @param respawn (Optional) True to respawn client on infect.
1168 */
1169stock InfectManualInfect(client, targets[], count, bool:respawnoverride = false, bool:respawn = false)
1170{
1171 new bool:zombiespawned = g_bZombieSpawned;
1172
1173 // If zombie hasn't spawned, then make targetted player(s) mother zombies.
1174 if (!zombiespawned)
1175 {
1176 // Stop mother infect timer.
1177 if (tInfect != INVALID_HANDLE)
1178 {
1179 KillTimer(tInfect);
1180 tInfect = INVALID_HANDLE;
1181 }
1182
1183 // Move all clients to CT
1184 for (new x = 1; x <= MaxClients; x++)
1185 {
1186 // If client isn't in-game, then stop.
1187 if (!IsClientInGame(x))
1188 {
1189 continue;
1190 }
1191
1192 // If client is dead, then stop.
1193 if (!IsPlayerAlive(x))
1194 {
1195 continue;
1196 }
1197
1198 // Switch client to CT team.
1199 CS_SwitchTeam(x, CS_TEAM_CT);
1200 }
1201
1202 // Tell the plugin a mother zombie has spawned.
1203 g_bZombieSpawned = true;
1204 }
1205
1206 decl String:targetname[MAX_NAME_LENGTH];
1207
1208 // x = Client index.
1209 for (new x = 0; x < count; x++)
1210 {
1211 // Get client's name for later use.
1212 GetClientName(targets[x], targetname, sizeof(targetname));
1213
1214 // Check if client is a human before turning into zombie.
1215 if (!InfectIsClientHuman(targets[x]))
1216 {
1217 // If there was only 1 player targetted, then let admin know the command was unsuccessful.
1218 if (count == 1)
1219 {
1220 // Tell admin command was unsuccessful.
1221 TranslationReplyToCommand(client, "Infect command infect unsuccessful", targetname);
1222 }
1223
1224 continue;
1225 }
1226
1227 // If zombie hasn't spawned, then make targetted player(s) mother zombies.
1228 if (!zombiespawned)
1229 {
1230 // Turn client into a mother zombie.
1231 InfectHumanToZombie(targets[x], _, true, respawnoverride, respawn);
1232
1233 // If there was only 1 player targetted, then let admin know the outcome of the command.
1234 if (count == 1)
1235 {
1236 TranslationReplyToCommand(client, "Infect command infect mother successful", targetname);
1237 }
1238
1239 continue;
1240 }
1241
1242 // Turn client into a zombie.
1243 InfectHumanToZombie(targets[x], _, false, respawnoverride, respawn);
1244
1245 // If there was only 1 player targetted, then let admin know the outcome of the command.
1246 if (count == 1)
1247 {
1248 TranslationReplyToCommand(client, "Infect command infect successful", targetname);
1249 }
1250 }
1251}
1252
1253/**
1254 * Infecting a client manually (via zr_human or the "Zombie Management" menu)
1255 *
1256 * @param client The client index changing a zombie to human.
1257 * @param targets Array containing all clients to make human.
1258 * @param count The number of clients in the array.
1259 * @param respawn (Optional) True to respawn client upon changing to human.
1260 * @param protect (Optional) True to protect client upon changing to human.
1261 */
1262stock InfectManualHuman(client, targets[], count, bool:respawn = false, bool:protect = false)
1263{
1264 decl String:targetname[MAX_NAME_LENGTH];
1265
1266 // x = Client index.
1267 for (new x = 0; x < count; x++)
1268 {
1269 // Get client's name for later use.
1270 GetClientName(targets[x], targetname, sizeof(targetname));
1271
1272 // Check if client is a human before turning into zombie.
1273 if (InfectIsClientInfected(targets[x]))
1274 {
1275 // Turn client into a zombie.
1276 InfectZombieToHuman(targets[x], respawn, protect);
1277
1278 // If there was only 1 player targetted, then let admin know the outcome of the command.
1279 if (count == 1)
1280 {
1281 // Tell admin command was successful.
1282 TranslationReplyToCommand(client, "Infect command human successful", targetname);
1283 }
1284 }
1285 else
1286 {
1287 // If there was only 1 player targetted, then let admin know the command was unsuccessful.
1288 if (count == 1)
1289 {
1290 // Tell admin command was unsuccessful.
1291 TranslationReplyToCommand(client, "Infect command human unsuccessful", targetname);
1292 }
1293 }
1294 }
1295}
1296
1297/**
1298 * Command callback (zr_infect)
1299 * Infects a client.
1300 *
1301 * @param client The client index.
1302 * @param argc Argument count.
1303 */
1304public Action:InfectInfectCommand(client, argc)
1305{
1306 // Check if privileged.
1307 if (!ZRIsClientPrivileged(client, OperationType_Generic))
1308 {
1309 TranslationReplyToCommand(client, "No access to command");
1310 return Plugin_Handled;
1311 }
1312
1313 // If not enough arguments given, then stop.
1314 if (argc < 1)
1315 {
1316 TranslationReplyToCommand(client, "Infect command infect syntax");
1317 return Plugin_Handled;
1318 }
1319
1320 decl String:target[MAX_NAME_LENGTH], String:targetname[MAX_NAME_LENGTH];
1321 new targets[MAXPLAYERS], bool:tn_is_ml, result;
1322
1323 // Get targetname.
1324 GetCmdArg(1, target, sizeof(target));
1325
1326 // Find a target.
1327 result = ProcessTargetString(target, client, targets, sizeof(targets), COMMAND_FILTER_ALIVE , targetname, sizeof(targetname), tn_is_ml);
1328
1329 // Check if there was a problem finding a client.
1330 if (result <= 0)
1331 {
1332 ZRReplyToTargetError(client, result);
1333 return Plugin_Handled;
1334 }
1335
1336 // Get respawn parameter.
1337 decl String:strRespawn[8];
1338 GetCmdArg(2, strRespawn, sizeof(strRespawn));
1339
1340 new bool:respawnoverride, bool:respawn;
1341
1342 // If parameter exists then cast it into a bool and feed it to infect function.
1343 if (strRespawn[0])
1344 {
1345 respawnoverride = true;
1346 respawn = bool:StringToInt(strRespawn);
1347 }
1348
1349 // Infect player.
1350 InfectManualInfect(client, targets, result, respawnoverride, respawn);
1351
1352 return Plugin_Handled;
1353}
1354
1355/**
1356 * Command callback (zr_human)
1357 * Turns a client into a human.
1358 *
1359 * @param client The client index.
1360 * @param argc Argument count.
1361 */
1362public Action:InfectHumanCommand(client, argc)
1363{
1364 // Check if privileged.
1365 if (!ZRIsClientPrivileged(client, OperationType_Generic))
1366 {
1367 TranslationReplyToCommand(client, "No access to command");
1368 return Plugin_Handled;
1369 }
1370
1371 // If not enough arguments given, then stop.
1372 if (argc < 1)
1373 {
1374 TranslationReplyToCommand(client, "Infect command human syntax");
1375 return Plugin_Handled;
1376 }
1377
1378 decl String:target[MAX_NAME_LENGTH], String:targetname[MAX_NAME_LENGTH];
1379 new targets[MAXPLAYERS], bool:tn_is_ml, result;
1380
1381 // Get targetname.
1382 GetCmdArg(1, target, sizeof(target));
1383
1384 // Find a target.
1385 result = ProcessTargetString(target, client, targets, sizeof(targets), COMMAND_FILTER_ALIVE , targetname, sizeof(targetname), tn_is_ml);
1386
1387 // Check if there was a problem finding a client.
1388 if (result <= 0)
1389 {
1390 ZRReplyToTargetError(client, result);
1391 return Plugin_Handled;
1392 }
1393
1394 // Get respawn&protect parameters
1395 decl String:strRespawn[8], String:strProtect[8];
1396 GetCmdArg(2, strRespawn, sizeof(strRespawn));
1397 GetCmdArg(3, strProtect, sizeof(strProtect));
1398
1399 // If parameter exists then cast it into a bool and feed it to "humanize" function.
1400 new bool:respawn = (strRespawn[0]) ? (bool:StringToInt(strRespawn)) : false;
1401 new bool:protect = (strProtect[0]) ? (bool:StringToInt(strProtect)) : false;
1402
1403 // Turn client into human.
1404 InfectManualHuman(client, targets, result, respawn, protect);
1405
1406 return Plugin_Handled;
1407}
1408
1409/**
1410 * Converts a string to an infection mode.
1411 *
1412 * @param mode Mode string to convert.
1413 *
1414 * @return Infection mode or InfectMode_Invalid on error.
1415 */
1416InfectMode:InfectStringToMode(const String:mode[])
1417{
1418 if (strlen(mode) == 0)
1419 {
1420 return InfectMode_Invalid;
1421 }
1422
1423 if (StrEqual(mode, "dynamic", false))
1424 {
1425 return InfectMode_Dynamic;
1426 }
1427 else if (StrEqual(mode, "absolute", false))
1428 {
1429 return InfectMode_Absolute;
1430 }
1431 else if (StrEqual(mode, "range", false))
1432 {
1433 return InfectMode_Range;
1434 }
1435
1436 return InfectMode_Invalid;
1437}
1438
1439/**
1440 * Gets and validates the infection mode. On error it will log a warning.
1441 *
1442 * @return Infection mode or InfectMode_Invalid on error.
1443 */
1444InfectMode:InfectGetModeOrFail()
1445{
1446 new String:modeName[16];
1447 GetConVarString(g_hCvarsList[CVAR_INFECT_MZOMBIE_MODE], modeName, sizeof(modeName));
1448
1449 new InfectMode:mode = InfectStringToMode(modeName);
1450 new ratio = GetConVarInt(g_hCvarsList[CVAR_INFECT_MZOMBIE_RATIO]);
1451 new min = GetConVarInt(g_hCvarsList[CVAR_INFECT_MZOMBIE_MIN]);
1452 new max = GetConVarInt(g_hCvarsList[CVAR_INFECT_MZOMBIE_MAX]);
1453
1454 // Validate.
1455 switch (mode)
1456 {
1457 case InfectMode_Invalid:
1458 {
1459 LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Infect, "Config Validation", "Warning: Invalid infection mode (\"%s\"). Falling back to one mother zombie.", modeName);
1460 }
1461 case InfectMode_Dynamic:
1462 {
1463 if (ratio < 0)
1464 {
1465 LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Infect, "Config Validation", "Warning: Invalid infection ratio (\"%d\"). Must be zero or positive in dynamic mode. Falling back to one mother zombie.", ratio);
1466 return InfectMode_Invalid;
1467 }
1468 }
1469 case InfectMode_Absolute:
1470 {
1471 if (ratio == 0)
1472 {
1473 LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Infect, "Config Validation", "Warning: Invalid infection ratio (\"%d\"). Must be nonzero in absolute mode. Falling back to one mother zombie.", ratio);
1474 return InfectMode_Invalid;
1475 }
1476 }
1477 case InfectMode_Range:
1478 {
1479 new bool:failed = false;
1480 if (min <= 0)
1481 {
1482 LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Infect, "Config Validation", "Warning: Invalid infection range (\"%d\"). Cvar zr_infect_mzombie_min must be nonzero and positive. Falling back to one mother zombie.", min);
1483 failed = true;
1484 }
1485 if (max <= 0)
1486 {
1487 LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Infect, "Config Validation", "Warning: Invalid infection range (\"%d\"). Cvar zr_infect_mzombie_max must be nonzero and positive. Falling back to one mother zombie.", max);
1488 failed = true;
1489 }
1490 if (min > max || max < min)
1491 {
1492 LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Infect, "Config Validation", "Warning: Infection range values are overlapping or reversed. Check zr_infect_mzombie_min and zr_infect_mzombie_min. Falling back to one mother zombie.");
1493 failed = true;
1494 }
1495
1496 if (failed)
1497 {
1498 return InfectMode_Invalid;
1499 }
1500 }
1501 }
1502
1503 return mode;
1504}