]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/mmm/sv_mmm.qc
21ef275f5ed09e6c03daca56c78925855fd78133
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / gamemodes / gamemode / mmm / sv_mmm.qc
1 #include "sv_mmm.qh"
2
3 //set g_mmm_sleuth_count 0.125 "number of players who will become sleuths, set between 0 and 0.9 to use a multiplier of the current players, or 1 and above to specify an exact number of players"
4 //float autocvar_g_mmm_sleuth_count = 0.125; //I don't think that it won't be used...
5 float autocvar_g_mmm_civilian_count = 0.625;
6 //float autocvar_g_mmm_murderer_count = 0.25;
7 float autocvar_g_mmm_round_timelimit = 180;
8 float autocvar_g_mmm_warmup = 10;
9 bool autocvar_g_mmm_punish_teamkill = false;
10 bool autocvar_g_mmm_reward_civilian = true;
11 bool autocvar_g_mmm_reward_sleuth = true; //sleuth reward if investigated corpses
12 float autocvar_g_mmm_max_karma_points = 1000; //LegendGuard sets Karma points 21-02-2021
13 float autocvar_g_mmm_min_karma_points = 400;
14 int autocvar_g_mmm_karma_bankick_tool = 0; //LegendGuard sets a ban tool for server admins 11-03-2021
15 float autocvar_g_mmm_karma_bantime = 1800; //karma ban seconds
16 bool autocvar_g_mmm_karma_damageactive = true; //LegendGuard sets Karma damage setting if active 20-03-2021
17 float autocvar_g_mmm_karma_severity = 0.25;
18 float autocvar_g_mmm_karma_damagepunishmentdeal = 20; //LegendGuard sets Karma punishment damage setting if player kills an ally 28-03-2021
19 // Sleuth is a created team, this team is added inside Civilians team
20
21 void mmm_FakeTimeLimit(entity e, float t)
22 {
23         if(!IS_REAL_CLIENT(e))
24                 return;
25 #if 0
26         msg_entity = e;
27         WriteByte(MSG_ONE, 3); // svc_updatestat
28         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
29         if(t < 0)
30                 WriteCoord(MSG_ONE, autocvar_timelimit);
31         else
32                 WriteCoord(MSG_ONE, (t + 1) / 60);
33 #else
34         STAT(MMM_ROUNDTIMER, e) = t;
35 #endif
36 }
37
38 void nades_Clear(entity player);
39
40 void karma_Control(entity it)
41 {
42         float masksize = autocvar_g_ban_default_masksize;
43         float bantime = autocvar_g_mmm_karma_bantime;
44         if(it.karmapoints >= autocvar_g_mmm_max_karma_points)
45         {
46                 //Resets karmapoints to maintain the maximum
47                 //PrintToChatAll("^3REWARD ^1MAXIMUM RESET");
48                 GameRules_scoring_add(it, MMM_KARMA, autocvar_g_mmm_max_karma_points - it.karmapoints);
49                 it.karmapoints = autocvar_g_mmm_max_karma_points;
50         }
51         else if(it.karmapoints <= autocvar_g_mmm_min_karma_points)
52         {
53                 switch (autocvar_g_mmm_karma_bankick_tool)
54                 {
55                         //force to spec
56                         case 0: PutObserverInServer(it); return;
57                         //kick
58                         case 1: dropclient(it); return;
59                         //ban and kick
60                         case 2: Ban_KickBanClient(it, bantime, masksize, "Too low karma"); return;
61                         //force to spec
62                         default: PutObserverInServer(it); return;
63                 }
64         }
65 }
66
67 void karmaLoseDifference(entity attacker, entity target)
68 {
69         if (autocvar_g_mmm_karma_severity <= 0.09)
70                 autocvar_g_mmm_karma_severity = 0.1;
71         else if (autocvar_g_mmm_karma_severity > 1)
72                 autocvar_g_mmm_karma_severity = 1;
73         
74         //BASIC MATH THEORY: example: 1000 * 0.3 * (0.1 + 0.4) * 0.25 // karma points reduce when player attacked to other player
75         if (target.karmapoints < attacker.karmapoints)
76         {
77                 float decreasekarma = - ( target.karmapoints * random() * ( 0.1 + random() ) * autocvar_g_mmm_karma_severity );
78                 GameRules_scoring_add(attacker, MMM_KARMA, decreasekarma);
79                 attacker.karmapoints = attacker.karmapoints + decreasekarma;
80         }
81         else if (target.karmapoints > attacker.karmapoints)
82         {
83                 float decreasekarma = - ( target.karmapoints * random() * ( 0.1 + random() ) * autocvar_g_mmm_karma_severity );
84                 GameRules_scoring_add(attacker, MMM_KARMA, decreasekarma);
85                 attacker.karmapoints = attacker.karmapoints + decreasekarma;
86         }
87         else
88         {
89                 float decreasekarma = - ( target.karmapoints * random() * ( 0.1 + random() ) * autocvar_g_mmm_karma_severity );
90                 GameRules_scoring_add(attacker, MMM_KARMA, decreasekarma);
91                 attacker.karmapoints = attacker.karmapoints + decreasekarma;
92         }
93 }
94
95 void karmaWinDifference(entity it)
96 {
97         GameRules_scoring_add(it, SCORE, 1); // reward civilians who make it to the end of the round time limit
98         float increasekarma = ( autocvar_g_mmm_min_karma_points * random() * ( 0.1 + random() ) * 0.12 );
99         GameRules_scoring_add(it, MMM_KARMA, increasekarma);
100         it.karmapoints = it.karmapoints + increasekarma;
101         karma_Control(it);
102 }
103
104 void mmm_UpdateScores(bool timed_out)
105 {
106         // give players their hard-earned kills now that the round is over
107         FOREACH_CLIENT(true,
108         {
109                 it.totalfrags += it.mmm_validkills;
110                 if(it.mmm_validkills)
111                 {
112                         GameRules_scoring_add(it, SCORE, it.mmm_validkills);
113                 }
114                 it.mmm_validkills = 0;
115                 // player survived the round
116                 if(IS_PLAYER(it) && !IS_DEAD(it)) // LegendGuard adds something for Karma 21-02-2021
117                 {
118                         if((autocvar_g_mmm_reward_civilian && timed_out && it.mmm_status == MMM_STATUS_CIVILIAN) 
119                         || (autocvar_g_mmm_reward_civilian && !timed_out && it.mmm_status == MMM_STATUS_CIVILIAN))
120                         {
121                                 karmaWinDifference(it);
122                                 //PrintToChatAll(sprintf("^2REWARD ^7it.karmapoints: ^1%f", it.karmapoints));
123                         }
124
125                         //Sleuth reward after investigated a corpse
126                         if((autocvar_g_mmm_reward_sleuth && timed_out && it.mmm_status == MMM_STATUS_SLEUTH) 
127                         || (autocvar_g_mmm_reward_sleuth && !timed_out && it.mmm_status == MMM_STATUS_SLEUTH))
128                         {
129                                 if (it.investigated == true)
130                                 {
131                                         karmaWinDifference(it);
132                                         it.investigated = false;
133                                 }
134                         }
135
136                         if(it.mmm_status == MMM_STATUS_MURDERER)
137                         {
138                                 karmaWinDifference(it);
139                                 //PrintToChatAll(sprintf("^1MURDERER ^7it.karmapoints: ^1%f", it.karmapoints));
140                         }
141                 }
142         });
143 }
144
145 float mmm_CheckWinner()
146 {
147         if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
148         {
149                 // if the match times out, civilians win too!
150                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MMM_CIVILIAN_WIN);
151                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MMM_CIVILIAN_WIN);
152                 FOREACH_CLIENT(true,
153                 {
154                         if(IS_PLAYER(it))
155                                 nades_Clear(it);
156                         mmm_FakeTimeLimit(it, -1);
157                         karma_Control(it);
158                 });
159
160                 mmm_UpdateScores(true);
161
162                 allowed_to_spawn = false;
163                 game_stopped = true;
164                 round_handler_Init(5, autocvar_g_mmm_warmup, autocvar_g_mmm_round_timelimit);
165                 return 1;
166         }
167
168         int civilian_count = 0, murderer_count = 0, sleuth_count = 0;
169         FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
170         {
171                 if(it.mmm_status == MMM_STATUS_CIVILIAN)
172                         civilian_count++;
173                 else if(it.mmm_status == MMM_STATUS_MURDERER)
174                         murderer_count++;
175                 else if(it.mmm_status == MMM_STATUS_SLEUTH) //LegendGuard adds sleuth_count 20-02-2021 
176                         sleuth_count++;
177         });
178         if(civilian_count > 0 && murderer_count > 0)
179         {
180                 return 0;
181         }
182
183         if(murderer_count > 0) // murderers win
184         {
185                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MMM_MURDERER_WIN);
186                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MMM_MURDERER_WIN);
187         }
188         else if(civilian_count > 0) // civilians win
189         {
190                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MMM_CIVILIAN_WIN);
191                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MMM_CIVILIAN_WIN);
192         }
193         else if (sleuth_count > 0 && civilian_count > 0) // sleuths are same as civilians win
194         {
195                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MMM_CIVILIAN_WIN);
196                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MMM_CIVILIAN_WIN);
197         }
198         else
199         {
200                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED);
201                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED);
202         }
203
204         mmm_UpdateScores(false);
205
206         allowed_to_spawn = false;
207         game_stopped = true;
208         round_handler_Init(5, autocvar_g_mmm_warmup, autocvar_g_mmm_round_timelimit);
209
210         FOREACH_CLIENT(true,
211         {
212                 if(IS_PLAYER(it))
213                 {
214                         it.respawn_flags = RESPAWN_SILENT; //CSQC print output respawn lib.qh error fix
215                         nades_Clear(it);
216                 }
217                 mmm_FakeTimeLimit(it, -1);
218                 karma_Control(it);
219         });
220
221         return 1;
222 }
223
224 void mmm_RoundStart()
225 {
226         allowed_to_spawn = boolean(warmup_stage);
227         int playercount = 0;
228
229         FOREACH_CLIENT(true,
230         {
231                 if(IS_PLAYER(it) && !IS_DEAD(it))
232                 {
233                         ++playercount;
234                         it.mmm_status = MMM_STATUS_CIVILIAN;
235                 }
236                 else
237                         it.mmm_status = 0; // this is mostly a safety check; if a client manages to somehow maintain a mmm status, clear it before the round starts!
238                 it.mmm_validkills = 0;
239         });
240         
241         int civilian_count = bound(1, ((autocvar_g_mmm_civilian_count >= 1) ? autocvar_g_mmm_civilian_count : floor(playercount * autocvar_g_mmm_civilian_count)), playercount - 1); // 20%, but ensure at least 1 and less than total
242         int total_civilians = 0;
243         //int murderer_count = bound(1, ((autocvar_g_mmm_murderer_count >= 1) ? autocvar_g_mmm_murderer_count : floor(playercount * autocvar_g_mmm_murderer_count)), playercount - 1); // 20%, but ensure at least 1 and less than total
244         int total_murderers = 0;
245         //int sleuth_count = bound(1, ((autocvar_g_mmm_sleuth_count >= 1) ? autocvar_g_mmm_sleuth_count : floor(playercount * autocvar_g_mmm_sleuth_count)), playercount - 1); // 20%, but ensure at least 1 and less than total
246         int total_sleuths = 0;
247
248         //civilians TOTAL
249         FOREACH_CLIENT_RANDOM(IS_PLAYER(it) && !IS_DEAD(it),
250         {
251                 if(total_civilians >= civilian_count)
252                         break;
253                 //LegendGuard fixes the round start again 22-03-2021
254                 total_civilians++;
255                 if (total_civilians <= 1)
256                 {
257                         if (total_murderers <= 1)
258                         {
259                                 total_murderers++;
260                                 it.mmm_status = MMM_STATUS_MURDERER;
261                         }
262                 }
263                 else if (total_civilians == 2)
264                 {
265                         if (total_sleuths >= 1)
266                                 break;
267                         else
268                         {
269                                 total_sleuths++;
270                                 it.mmm_status = MMM_STATUS_SLEUTH;
271                         }
272                 }
273                 else if (total_civilians == 5)
274                 {
275                         if (total_murderers <= 2)
276                                 break;
277                         else
278                         {
279                                 total_murderers++;
280                                 it.mmm_status = MMM_STATUS_MURDERER;
281                         }
282                 }
283                 else if (total_civilians == 6)
284                 {
285                         if (total_sleuths >= 2)
286                                 break;
287                         else
288                         {
289                                 total_sleuths++;
290                                 it.mmm_status = MMM_STATUS_SLEUTH;
291                         }
292                 }
293                 else if (total_civilians == 7)
294                 {
295                         if (total_sleuths >= 3)
296                                 break;
297                         else if (total_murderers == 3)
298                         {
299                                 total_murderers++;
300                                 it.mmm_status = MMM_STATUS_MURDERER;
301                         }
302                         else
303                         {
304                                 total_sleuths++;
305                                 it.mmm_status = MMM_STATUS_SLEUTH;
306                         }
307                 }
308                 else if (total_civilians >= 8)
309                 {
310                         if (total_sleuths >= 4)
311                                 break;
312                         else if (total_murderers == 4)
313                         {
314                                 total_murderers++;
315                                 it.mmm_status = MMM_STATUS_MURDERER;
316                         }
317                         else
318                         {
319                                 total_sleuths++;
320                                 it.mmm_status = MMM_STATUS_SLEUTH;
321                         }
322                 }
323                 else
324                 {
325                         total_murderers++; 
326                         it.mmm_status = MMM_STATUS_MURDERER;
327                 }
328         });
329
330         FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
331         {
332                 float totalmeankarma = ((autocvar_g_mmm_max_karma_points + autocvar_g_mmm_min_karma_points + it.karmapoints) / 3);
333                 karma_Control(it);
334                 it.activekillerrole = false;
335
336                 if(it.mmm_status == MMM_STATUS_CIVILIAN)
337                 {
338                         if (it.karmapoints <= totalmeankarma)
339                         {
340                                 centerprint(it, strcat(BOLD_OPERATOR, "^1KARMA WARNING!\n^3Here, have the Rifle!"));
341                                 GiveWeapon(it, WEP_RIFLE.m_id, OP_PLUS, 1);
342                         }
343                         //Gives Mine Layer weapon to the player
344                         GiveWeapon(it, WEP_MINE_LAYER.m_id, OP_PLUS, 1);
345                         Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_MMM_CIVILIAN);
346                         Send_Notification(NOTIF_ONE_ONLY, it, MSG_INFO, INFO_MMM_CIVILIAN);
347                         //PrintToChatAll(sprintf("^1DEBUG^7: %s is ^2Civilian^7!", it.netname));
348                 }
349                 else if(it.mmm_status == MMM_STATUS_MURDERER)
350                 {
351                         if (it.karmapoints <= totalmeankarma)
352                         {
353                                 centerprint(it, strcat(BOLD_OPERATOR, "^1KARMA WARNING!\n^3Here, have the Rifle!"));
354                                 GiveWeapon(it, WEP_RIFLE.m_id, OP_PLUS, 1);
355                         }
356                         //Gives Mine Layer weapon to the player
357                         GiveWeapon(it, WEP_MINE_LAYER.m_id, OP_PLUS, 1);
358                         Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_MMM_MURDERER);
359                         Send_Notification(NOTIF_ONE_ONLY, it, MSG_INFO, INFO_MMM_MURDERER);
360                         //PrintToChatAll(sprintf("^1DEBUG^7: %s is ^1Murderer^7!", it.netname));
361                 }
362                 else if(it.mmm_status == MMM_STATUS_SLEUTH)
363                 {
364                         if (it.karmapoints <= totalmeankarma)
365                         {
366                                 centerprint(it, strcat(BOLD_OPERATOR, "^1KARMA WARNING!\n^3Here, have the Rifle!"));
367                                 GiveWeapon(it, WEP_RIFLE.m_id, OP_PLUS, 1);
368                         }
369                         //Gives Shockwave and Mine Layer weapon to the player
370                         GiveWeapon(it, WEP_SHOCKWAVE.m_id, OP_PLUS, 1);
371                         GiveWeapon(it, WEP_MINE_LAYER.m_id, OP_PLUS, 1);
372                         Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_MMM_SLEUTH);
373                         Send_Notification(NOTIF_ONE_ONLY, it, MSG_INFO, INFO_MMM_SLEUTH);
374                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MMM_WHOISSLEUTH, it.netname);
375                 }
376                 mmm_FakeTimeLimit(it, round_handler_GetEndTime());
377         });
378 }
379
380 bool mmm_CheckPlayers()
381 {
382         static int prev_missing_players;
383         allowed_to_spawn = true;
384         int playercount = 0;
385
386         FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
387         {       
388                 //PrintToChatAll(sprintf("it.karmapoints ^5begin: ^3%f",it.karmapoints));
389                 //Karma points start
390                 if (it.karmastarted != true)
391                 {
392                         GameRules_scoring_add(it, MMM_KARMA, autocvar_g_mmm_max_karma_points - it.karmapoints);
393                         it.karmapoints = autocvar_g_mmm_max_karma_points;
394                         it.karmastarted = true;
395                 }
396                 karma_Control(it);
397                 ++playercount;
398                 //PrintToChatAll(sprintf("it.karmapoints ^6end: ^3%f",it.karmapoints));
399         });
400
401         if (playercount >= 2)
402         {
403                 if(prev_missing_players > 0)
404                         Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS);
405                 prev_missing_players = -1;
406                 return true;
407         }
408
409         if(playercount == 0)
410         {
411                 if(prev_missing_players > 0)
412                         Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS);
413                 prev_missing_players = -1;
414                 return false;
415         }
416
417         // if we get here, only 1 player is missing
418         if(prev_missing_players != 1)
419         {
420                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_PLAYERS, 1);
421                 prev_missing_players = 1;
422         }
423         return false;
424 }
425
426 bool mmm_isEliminated(entity e)
427 {
428         if(e.caplayer == 1 && (IS_DEAD(e) || e.frags == FRAGS_PLAYER_OUT_OF_GAME))
429                 return true;
430         if(e.caplayer == 0.5)
431                 return true;
432         return false;
433 }
434
435 void mmm_Initialize() // run at the start of a match, initiates game mode
436 {
437         GameRules_scoring(0, SFL_SORT_PRIO_PRIMARY, 0, {
438                 field(SP_MMM_KARMA, "karma", SFL_SORT_PRIO_SECONDARY); //LegendGuard adds Karma points in the scoreboard 22-02-2021
439         });
440
441         allowed_to_spawn = true;
442         round_handler_Spawn(mmm_CheckPlayers, mmm_CheckWinner, mmm_RoundStart);
443         round_handler_Init(5, autocvar_g_mmm_warmup, autocvar_g_mmm_round_timelimit);
444         EliminatedPlayers_Init(mmm_isEliminated);
445 }
446
447 void checkWeaponDeathtype(entity target, float deathtype)
448 {
449         switch (deathtype)
450         {
451                 case WEP_ARC.m_id: case 276: case 788: target.killedwithweapon = "Impacted by the Arc's electric shock"; return;
452                 case WEP_BLASTER.m_id: case 513: target.killedwithweapon = "Blasted by the Blaster"; return;
453                 case WEP_CRYLINK.m_id: case 263: case 519: target.killedwithweapon = "Shot by the Crylink"; return;
454                 case WEP_DEVASTATOR.m_id: case 522: case 1546: target.killedwithweapon = "Bombarded by the Devastator"; return;
455                 case WEP_ELECTRO.m_id: case 262: case 518: case 1542: target.killedwithweapon = "Electrocuted by the Electro"; return;
456                 case WEP_FIREBALL.m_id: case 273: case 529: case 1297: target.killedwithweapon = "Burned by the Fireball"; return;
457                 case WEP_HAGAR.m_id: case 265: target.killedwithweapon = "Gunned by the Hagar"; return;
458                 case WEP_HLAC.m_id: case 270: case 526: target.killedwithweapon = "Cut down with the HLAC"; return;
459                 case WEP_HOOK.m_id: case 1805: target.killedwithweapon = "Caught in Hook gravity bomb"; return;
460                 case WEP_MACHINEGUN.m_id: target.activekillerrole = true; target.killedwithweapon = "Riddled full of holes by the Machine Gun"; return;
461                 case WEP_MINE_LAYER.m_id: case 517: case 1541: target.killedwithweapon = "Exploited by the Mine Layer"; return;
462                 case WEP_MORTAR.m_id: case 516: case 1284: target.killedwithweapon = "Blew up with the Mortar"; return;
463                 case WEP_OVERKILL_NEX.m_id: target.killedwithweapon = "Sniped by the Overkill Nex"; return;
464                 case WEP_RIFLE.m_id: case 272: target.activekillerrole = true; target.killedwithweapon = "Sniped by the Rifle"; return;
465                 case WEP_SEEKER.m_id: case 274: case 786: target.killedwithweapon = "Blasted by the Seeker"; return;
466                 case WEP_SHOCKWAVE.m_id: target.killedwithweapon = "Gunned down by the Shockwave"; return;
467                 case 275: target.killedwithweapon = "Knocked by the Shockwave"; return;
468                 case WEP_SHOTGUN.m_id: target.activekillerrole = true; target.killedwithweapon = "Shot by Shotgun"; return;
469                 case 258: target.killedwithweapon = "Knocked by the Shotgun"; return;
470                 case WEP_TUBA.m_id: target.killedwithweapon = "Ear pain by the @!#%'n Tuba"; return;
471                 case WEP_VAPORIZER.m_id: case 257: case 769: target.killedwithweapon = "Sniped by the Vaporizer"; return;
472                 case WEP_VORTEX.m_id: target.killedwithweapon = "Sniped by the Vortex"; return;
473                 case DEATH_FALL.m_id: target.killedwithweapon = "Fall"; return;
474                 case DEATH_FIRE.m_id: target.killedwithweapon = "Burned with the fire"; return;
475                 case DEATH_LAVA.m_id: target.killedwithweapon = "Burned in lava"; return;
476                 case DEATH_MIRRORDAMAGE.m_id: target.killedwithweapon = "Suicide"; return;
477                 case DEATH_SLIME.m_id: target.killedwithweapon = "Melted in slime"; return;
478                 case DEATH_TELEFRAG.m_id: target.killedwithweapon = "Telefragged"; return;
479                 case DEATH_NADE.m_id: target.killedwithweapon = "Blown up by the nade"; return;
480                 case DEATH_NADE_NAPALM.m_id: target.killedwithweapon = "Burned by the Napalm nade"; return;
481                 case DEATH_NADE_ICE.m_id: target.killedwithweapon = "Frozen by the Ice nade"; return;
482                 case DEATH_NADE_HEAL.m_id: target.killedwithweapon = "Sucked by the Heal nade"; return;
483                 default: target.killedwithweapon = "Unknown"; return;
484         }
485 }
486
487 void ReduceKarmaPointsandFrags(entity frag_attacker, entity frag_target, float frag_deathtype, entity wep_ent)
488 {
489         karmaLoseDifference(frag_attacker, frag_target);
490         GiveFrags(frag_attacker, frag_target, ((autocvar_g_mmm_punish_teamkill) ? -1 : -2), frag_deathtype, wep_ent.weaponentity_fld);
491         frag_target.whokilled = frag_attacker.netname;
492 }
493
494 // ==============
495 // Hook Functions
496 // ==============
497
498 MUTATOR_HOOKFUNCTION(mmm, ClientObituary)
499 {
500         // LegendGuard's IDEA: To adjust the grade of severity of karma, 
501         // we could add if sentence per weapons and adjust each weapon attack
502         // its own grade. Instead doing random decrease grade 22-02-2021
503         
504         // in mmm, announcing a frag would tell everyone who the murderer is
505         entity frag_attacker = M_ARGV(1, entity);
506         entity frag_target = M_ARGV(2, entity);
507         
508         if(IS_PLAYER(frag_attacker) && frag_attacker != frag_target)
509         {
510                 float frag_deathtype = M_ARGV(3, float);
511                 entity wep_ent = M_ARGV(4, entity);
512                 
513                 //PrintToChatAll(strcat("deathtype var: ", ftos(frag_deathtype)));
514                 checkWeaponDeathtype(frag_target, frag_deathtype);
515                 // "team" kill, a point is awarded to the player by default so we must take it away plus an extra one
516                 // unless the player is going to be punished for suicide, in which case just remove one
517                 if(frag_attacker.mmm_status == frag_target.mmm_status)
518                 {
519                         //PrintToChatAll("^1DEBUG^7: A ^2PLAYER^7 has fragged a ^2PLAYER OF HIS OWN TEAM^7, TOO BAD!");
520                         ReduceKarmaPointsandFrags(frag_attacker, frag_target, frag_deathtype, wep_ent);
521                         switch (frag_attacker.mmm_status)
522                         {
523                                 case MMM_STATUS_CIVILIAN: frag_target.killerrole = "\n^3Killer role: ^2Civilian"; return;
524                                 case MMM_STATUS_MURDERER: frag_target.killerrole = "\n^3Killer role: ^1Murderer"; return;
525                                 case MMM_STATUS_SLEUTH: frag_target.killerrole = "\n^3Killer role: ^4Sleuth"; return;
526                                 default: return;
527                         }
528                         //PrintToChatAll(sprintf("frag_attacker.karmapoints: ^1%f", frag_attacker.karmapoints));
529                 }
530
531                 if(frag_attacker.mmm_status == MMM_STATUS_SLEUTH)
532                 {
533                         if (frag_target.mmm_status == MMM_STATUS_CIVILIAN || frag_target.mmm_status == MMM_STATUS_SLEUTH)
534                         {       
535                                 //PrintToChatAll("^1DEBUG^7: A ^4Sleuth^7 fragged an ^2Civilian^7/^4Sleuth^7, TOO BAD!");
536                                 ReduceKarmaPointsandFrags(frag_attacker, frag_target, frag_deathtype, wep_ent);
537                                 frag_target.killerrole = "\n^3Killer role: ^4Sleuth";
538                                 //PrintToChatAll(sprintf("frag_attacker.karmapoints: ^1%f", frag_attacker.karmapoints));
539                         }
540                         else
541                         {
542                                 frag_target.whokilled = frag_attacker.netname;
543                                 frag_target.killerrole = "\n^3Killer role: ^4Sleuth";
544                         }
545                 }
546
547                 if (frag_attacker.mmm_status == MMM_STATUS_CIVILIAN)
548                 {
549                         if (frag_target.mmm_status == MMM_STATUS_SLEUTH)
550                         {
551                                 //PrintToChatAll("^1DEBUG^7: An ^2Civilian^7 fragged a ^4Sleuth^7, TOO BAD!");
552                                 ReduceKarmaPointsandFrags(frag_attacker, frag_target, frag_deathtype, wep_ent);
553                                 frag_target.killerrole = "\n^3Killer role: ^2Civilian";
554                         }
555                         else
556                         {
557                                 frag_target.whokilled = frag_attacker.netname;
558                                 frag_target.killerrole = "\n^3Killer role: ^2Civilian";
559                         }
560                 }
561                 
562                 if (frag_attacker.mmm_status == MMM_STATUS_MURDERER)
563                 {
564                         if (frag_target.mmm_status == MMM_STATUS_CIVILIAN)
565                         {
566                                 frag_target.whokilled = frag_attacker.netname;
567                                 frag_target.killerrole = "\n^3Killer role: ^1Murderer";
568                         }
569                         else
570                         {
571                                 frag_target.whokilled = frag_attacker.netname;
572                                 frag_target.killerrole = "\n^3Killer role: ^1Murderer";
573                         }
574                 }
575                 //if mmm_status is 1, means civilian, 2 means murderer, 3 means sleuth
576                 //PrintToChatAll(sprintf("^1DEBUG^7: frag_attacker.mmm_status is ^3%s^7",ftos(frag_attacker.mmm_status)));
577                 //PrintToChatAll(sprintf("^1DEBUG^7: frag_target.mmm_status is ^3%s^7",ftos(frag_target.mmm_status)));
578         }
579         else
580         {
581                 float frag_deathtype = M_ARGV(3, float);
582                 checkWeaponDeathtype(frag_target, frag_deathtype);
583         }
584
585         M_ARGV(5, bool) = true; // anonymous attacker
586 }
587
588 //karma weapon damage, halve the damage attack when player has low karma 20-03-2021
589 MUTATOR_HOOKFUNCTION(mmm, Damage_Calculate)
590 {
591         entity attacker = M_ARGV(1, entity);
592         entity target = M_ARGV(2, entity);
593         float deathtype = M_ARGV(3, float);
594         float damage = M_ARGV(4, float);
595         vector force = M_ARGV(6, vector);
596         string corpsemessagestrcat = "";
597         string corpsemsginfo = "";
598
599         if (autocvar_g_mmm_karma_damageactive != false)
600         {
601                 if (IS_PLAYER(attacker))
602                 {
603                         if(target == attacker) // damage done to yourself
604                         {
605                                 damage /= autocvar_g_weapondamagefactor / (attacker.karmapoints / autocvar_g_mmm_max_karma_points);
606                                 force /= autocvar_g_weaponforcefactor / (attacker.karmapoints / autocvar_g_mmm_max_karma_points);
607                         }
608                         else if (target != attacker)
609                         {
610                                 damage /= autocvar_g_weapondamagefactor / (attacker.karmapoints / autocvar_g_mmm_max_karma_points);
611                                 force /= autocvar_g_weaponforcefactor / (attacker.karmapoints / autocvar_g_mmm_max_karma_points);
612                         }
613                         else
614                         {
615                                 damage *= autocvar_g_weapondamagefactor;
616                                 force *= autocvar_g_weaponforcefactor;
617                         }
618                 }
619         }
620
621         //CORPSE DETECTION SKILL 21-03-2021
622         if(IS_DEAD(target))
623         {
624                 //Shockwave weapon as radar gun to check the corpses 22-03-2021
625                 if(DEATH_ISWEAPON(deathtype, WEP_SHOCKWAVE))
626                 {
627                         if (target.killedwithweapon == "")
628                                 target.killedwithweapon = "UNKNOWN CAUSE";
629                         
630                         if (target.activekillerrole != true)
631                         {
632                                 target.killerrole = "";
633                                 target.activekillerrole = false;
634                         }
635
636                         string killedbyphrase = strcat("^3Killed by:^7 ", target.whokilled, target.killerrole); 
637                         string wepkilledphrase = strcat("^3Cause:^7 ", target.killedwithweapon);
638                         if (target.whokilled == "")
639                         {
640                                 killedbyphrase = "";
641                                 if (target.killedwithweapon == "")
642                                         wepkilledphrase = "^3Cause:^7 UNKNOWN CAUSE";
643                         }
644
645                         damage = 0;
646                         force = '0 0 0';
647                         if (target.mmm_status == MMM_STATUS_CIVILIAN)
648                         {
649                                 //try to add centerprint message for chat privately if possible
650                                 corpsemessagestrcat = strcat("^5DEAD PLAYER DETAILS^7: \n^3Name:^7 ", target.netname, "\n^3Role: ^2Civilian", "\n", killedbyphrase, "\n", wepkilledphrase);
651                                 corpsemsginfo = strcat("^5DEAD PLAYER DETAILS^7: ^3Name:^7 ", target.netname, "; ^3Role: ^2Civilian", "; ", killedbyphrase, "; ", wepkilledphrase);
652                                 //centerprint(attacker, strcat(BOLD_OPERATOR, corpsemessagestrcat));
653                                 Send_Notification(NOTIF_ONE_ONLY, attacker, MSG_CENTER, CENTER_MMM_CORPSEDETECTION, corpsemessagestrcat);
654                                 Send_Notification(NOTIF_ONE_ONLY, attacker, MSG_INFO, INFO_MMM_CORPSEDETECTION, corpsemsginfo);
655                         }
656                         else if (target.mmm_status == MMM_STATUS_MURDERER)
657                         {
658                                 corpsemessagestrcat = strcat("^5DEAD PLAYER DETAILS^7: \n^3Name:^7 ", target.netname, "\n^3Role: ^1Murderer", "\n", killedbyphrase, "\n", wepkilledphrase);
659                                 corpsemsginfo = strcat("^5DEAD PLAYER DETAILS^7: ^3Name:^7 ", target.netname, "; ^3Role: ^1Murderer", "; ", killedbyphrase, "; ", wepkilledphrase);
660                                 //centerprint(attacker, strcat(BOLD_OPERATOR, corpsemessagestrcat));
661                                 Send_Notification(NOTIF_ONE_ONLY, attacker, MSG_CENTER, CENTER_MMM_CORPSEDETECTION, corpsemessagestrcat);
662                                 Send_Notification(NOTIF_ONE_ONLY, attacker, MSG_INFO, INFO_MMM_CORPSEDETECTION, corpsemsginfo);
663                         }
664                         else if (target.mmm_status == MMM_STATUS_SLEUTH)
665                         {
666                                 corpsemessagestrcat = strcat("^5DEAD PLAYER DETAILS^7: \n^3Name:^7 ", target.netname, "\n^3Role: ^4Sleuth", "\n", killedbyphrase, "\n", wepkilledphrase);
667                                 corpsemsginfo = strcat("^5DEAD PLAYER DETAILS^7: ^3Name:^7 ", target.netname, "; ^3Role: ^4Sleuth", "; ", killedbyphrase, "; ", wepkilledphrase);
668                                 //centerprint(attacker, strcat(BOLD_OPERATOR, corpsemessagestrcat));
669                                 Send_Notification(NOTIF_ONE_ONLY, attacker, MSG_CENTER, CENTER_MMM_CORPSEDETECTION, corpsemessagestrcat);
670                                 Send_Notification(NOTIF_ONE_ONLY, attacker, MSG_INFO, INFO_MMM_CORPSEDETECTION, corpsemsginfo);
671                         }
672                         attacker.investigated = true;
673                 }
674         }
675
676         M_ARGV(4, float) = damage;
677         M_ARGV(6, vector) = force;
678 }
679
680 MUTATOR_HOOKFUNCTION(mmm, PlayerPreThink)
681 {
682         entity player = M_ARGV(0, entity);
683         
684         if(IS_PLAYER(player) || player.caplayer)
685         {
686                 // update the scoreboard colour display to out the real killer at the end of the round
687                 // running this every frame to avoid cheats
688                 int plcolor = MMM_COLOR_CIVILIAN;
689                 if(player.mmm_status == MMM_STATUS_CIVILIAN && game_stopped) //Civilian status by default
690                         plcolor = MMM_COLOR_CIVILIAN;
691                 if(player.mmm_status == MMM_STATUS_MURDERER && game_stopped)
692                         plcolor = MMM_COLOR_MURDERER;
693                 //LegendGuard adds for Sleuth 21-02-2021
694                 if(player.mmm_status == MMM_STATUS_SLEUTH)// && game_stopped)
695                         plcolor = MMM_COLOR_SLEUTH;
696                 setcolor(player, plcolor);
697         }
698
699         //CORPSE FEATURE 10-03-2021
700         if (IS_DEAD(player))
701         {
702                 player.event_damage = func_null;
703                 //player.health = 0;
704                 player.solid = SOLID_CORPSE;
705                 set_movetype(player, MOVETYPE_STEP); //test with MOVETYPE_TOSS or MOVETYPE_WALK (it's like sliding object) or MOVETYPE_BOUNCE (maybe not good)
706         }
707 }
708
709 MUTATOR_HOOKFUNCTION(mmm, PlayerSpawn)
710 {
711         entity player = M_ARGV(0, entity);
712
713         player.mmm_status = 0;
714         player.mmm_validkills = 0;
715         player.caplayer = 1;
716         if (!warmup_stage)
717                 eliminatedPlayers.SendFlags |= 1;
718 }
719
720 MUTATOR_HOOKFUNCTION(mmm, ForbidSpawn)
721 {
722         entity player = M_ARGV(0, entity);
723
724         // spectators / observers that weren't playing can join; they are
725         // immediately forced to observe in the PutClientInServer hook
726         // this way they are put in a team and can play in the next round
727         if (!allowed_to_spawn && player.caplayer)
728                 return true;
729         return false;
730 }
731
732 MUTATOR_HOOKFUNCTION(mmm, PutClientInServer)
733 {
734         entity player = M_ARGV(0, entity);
735
736         if (!allowed_to_spawn && IS_PLAYER(player)) // this is true even when player is trying to join
737         {
738                 TRANSMUTE(Observer, player);
739                 if (CS(player).jointime != time && !player.caplayer) // not when connecting
740                 {
741                         player.caplayer = 0.5;
742                         Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_JOIN_LATE);
743                 }
744         }
745 }
746
747 MUTATOR_HOOKFUNCTION(mmm, reset_map_players)
748 {
749         FOREACH_CLIENT(true, {
750                 CS(it).killcount = 0;
751                 it.mmm_status = 0;
752                 mmm_FakeTimeLimit(it, -1); // restore original timelimit
753                 if (!it.caplayer && IS_BOT_CLIENT(it))
754                         it.caplayer = 1;
755                 if (it.caplayer)
756                 {
757                         TRANSMUTE(Player, it);
758                         it.caplayer = 1;
759                         it.respawn_flags = RESPAWN_SILENT; //CSQC print output respawn lib.qh error fix
760                         PutClientInServer(it);
761                 }
762         });
763         bot_relinkplayerlist();
764         return true;
765 }
766
767 MUTATOR_HOOKFUNCTION(mmm, reset_map_global)
768 {
769         allowed_to_spawn = true;
770         return true;
771 }
772
773 entity mmm_LastPlayerForTeam(entity this)
774 {
775         entity last_pl = NULL;
776         FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
777                 if (!IS_DEAD(it) && this.mmm_status == it.mmm_status)
778                 {
779                         if (!last_pl)
780                         {
781                                 last_pl = it;
782                         }
783                         else
784                                 return NULL;
785                 }
786         });
787         return last_pl;
788 }
789
790 void mmm_LastPlayerForTeam_Notify(entity this)
791 {
792         if (!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted())
793         {
794                 entity pl = mmm_LastPlayerForTeam(this);
795                 if (pl)
796                         Send_Notification(NOTIF_ONE_ONLY, pl, MSG_CENTER, CENTER_ALONE);
797         }
798 }
799
800 MUTATOR_HOOKFUNCTION(mmm, PlayerDies)
801 {
802         entity frag_attacker = M_ARGV(1, entity);
803         entity frag_target = M_ARGV(2, entity);
804         //float frag_deathtype = M_ARGV(3, float);
805
806         mmm_LastPlayerForTeam_Notify(frag_target);
807         if (!allowed_to_spawn)
808         {
809                 frag_target.respawn_flags = RESPAWN_DENY;
810                 // prevent unwanted sudden rejoin as spectator and movement of spectator camera
811                 frag_target.respawn_time = time + 2;
812         }
813         frag_target.respawn_flags |= RESPAWN_DENY;
814         frag_target.event_damage = func_null;
815         frag_target.health = 0;
816         
817         if (!warmup_stage)
818                 eliminatedPlayers.SendFlags |= 1;
819         
820         //if(frag_attacker.mmm_status == frag_target.mmm_status)
821         // killed an ally! punishment is sentenced
822         if(frag_attacker.mmm_status == MMM_STATUS_SLEUTH)
823         {
824                 if (frag_target.mmm_status == MMM_STATUS_CIVILIAN)
825                 {
826                         //PrintToChatAll("^1DEBUG^7: ^4SLEUTH ^1DAMAGE/DEAD^7 HAS TAKEN!");
827                         Damage(frag_attacker, frag_attacker, frag_attacker, autocvar_g_mmm_karma_damagepunishmentdeal, DEATH_MIRRORDAMAGE.m_id, DMG_NOWEP, frag_attacker.origin, '0 0 0');
828                 }
829         }
830         if (frag_attacker.mmm_status == MMM_STATUS_CIVILIAN)
831         {
832                 if (frag_target.mmm_status == MMM_STATUS_SLEUTH)
833                 {
834                         //PrintToChatAll("^1DEBUG^7: ^2CIVILIAN ^1DAMAGE/DEAD^7 HAS TAKEN!");
835                         Damage(frag_attacker, frag_attacker, frag_attacker, autocvar_g_mmm_karma_damagepunishmentdeal, DEATH_MIRRORDAMAGE.m_id, DMG_NOWEP, frag_attacker.origin, '0 0 0');
836                 }
837         }
838         if (frag_attacker.mmm_status == MMM_STATUS_MURDERER)
839         {
840                 if (frag_target.mmm_status == MMM_STATUS_MURDERER)
841                 {
842                         //PrintToChatAll("^1DEBUG^7: ^1MURDERER ^1DAMAGE/DEAD^7 HAS TAKEN!");
843                         Damage(frag_attacker, frag_attacker, frag_attacker, autocvar_g_mmm_karma_damagepunishmentdeal, DEATH_MIRRORDAMAGE.m_id, DMG_NOWEP, frag_attacker.origin, '0 0 0');
844                 }
845         }
846         return true;
847 }
848
849 MUTATOR_HOOKFUNCTION(mmm, ClientDisconnect)
850 {
851         entity player = M_ARGV(0, entity);
852
853         if (IS_PLAYER(player) && !IS_DEAD(player))
854                 mmm_LastPlayerForTeam_Notify(player);
855         return true;
856 }
857
858 MUTATOR_HOOKFUNCTION(mmm, MakePlayerObserver)
859 {
860         entity player = M_ARGV(0, entity);
861
862         if (IS_PLAYER(player) && !IS_DEAD(player))
863                 mmm_LastPlayerForTeam_Notify(player);
864         if (player.killindicator_teamchange == -2) // player wants to spectate
865                 player.caplayer = 0;
866         if (player.caplayer)
867                 player.frags = FRAGS_PLAYER_OUT_OF_GAME;
868         if (!warmup_stage)
869                 eliminatedPlayers.SendFlags |= 1;
870         if (!player.caplayer)
871         {
872                 player.mmm_validkills = 0;
873                 player.mmm_status = 0;
874                 mmm_FakeTimeLimit(player, -1); // restore original timelimit
875                 return false;  // allow team reset
876         }
877         return true;  // prevent team reset
878 }
879
880 MUTATOR_HOOKFUNCTION(mmm, Scores_CountFragsRemaining)
881 {
882         // announce remaining frags?
883         return true;
884 }
885
886 MUTATOR_HOOKFUNCTION(mmm, GiveFragsForKill, CBC_ORDER_FIRST)
887 {
888         entity frag_attacker = M_ARGV(0, entity);
889         if(!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted()) 
890                 frag_attacker.mmm_validkills += M_ARGV(2, float);
891         M_ARGV(2, float) = 0; // score will be given to the winner when the round ends
892         return true;
893 }
894
895 MUTATOR_HOOKFUNCTION(mmm, AddPlayerScore)
896 {
897         // add scorefield for scoreboard here
898         entity scorefield = M_ARGV(0, entity);
899         if(scorefield == SP_KILLS || scorefield == SP_DEATHS || scorefield == SP_SUICIDES || scorefield == SP_DMG || scorefield == SP_DMGTAKEN)
900                 M_ARGV(1, float) = 0; // don't report that the player has killed or been killed, that would out them as a murderer!
901 }
902
903 MUTATOR_HOOKFUNCTION(mmm, CalculateRespawnTime)
904 {
905         // no respawn calculations needed, player is forced to spectate anyway
906         return true;
907 }
908
909 MUTATOR_HOOKFUNCTION(mmm, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
910 {
911         FOREACH_CLIENT(IS_REAL_CLIENT(it), {
912                 if (IS_PLAYER(it) || it.caplayer == 1)
913                         ++M_ARGV(0, int);
914                 ++M_ARGV(1, int);
915         });
916         return true;
917 }
918
919 MUTATOR_HOOKFUNCTION(mmm, ClientCommand_Spectate)
920 {
921         entity player = M_ARGV(0, entity);
922
923         if (player.caplayer)
924         {
925                 // they're going to spec, we can do other checks
926                 if (autocvar_sv_spectate && (IS_SPEC(player) || IS_OBSERVER(player)))
927                         Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_LEAVE);
928                 return MUT_SPECCMD_FORCE;
929         }
930
931         return MUT_SPECCMD_CONTINUE;
932 }
933
934 MUTATOR_HOOKFUNCTION(mmm, GetPlayerStatus)
935 {
936         entity player = M_ARGV(0, entity);
937
938         return player.caplayer == 1;
939 }
940
941 MUTATOR_HOOKFUNCTION(mmm, BotShouldAttack)
942 {
943         entity bot = M_ARGV(0, entity);
944         entity targ = M_ARGV(1, entity);
945
946         if(targ.mmm_status == bot.mmm_status)
947         {
948                 return true;
949         }
950         
951         // LegendGuard fixed the problem of Sleuths and Civilians attacking each other 26-03-2021
952         if(bot.mmm_status == MMM_STATUS_SLEUTH)
953         {
954                 if(targ.mmm_status == MMM_STATUS_CIVILIAN)
955                         return true;
956         }
957 }