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