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