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