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