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