From: LegendaryGuard Date: Sat, 20 Feb 2021 15:27:29 +0000 (+0100) Subject: Added Trouble in Terrorist Town gamemode, using another way to recover the lost gamem... X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=6508e517cb51fdffac22a7686a2c0bbd91bd1213;p=xonotic%2Fxonotic-data.pk3dir.git Added Trouble in Terrorist Town gamemode, using another way to recover the lost gamemode, there are still things that need to be completed --- diff --git a/gamemodes-client.cfg b/gamemodes-client.cfg index c43b9d1d3..633b73fe2 100644 --- a/gamemodes-client.cfg +++ b/gamemodes-client.cfg @@ -32,6 +32,7 @@ alias cl_hook_gamestart_ka alias cl_hook_gamestart_ft alias cl_hook_gamestart_inv alias cl_hook_gamestart_duel +alias cl_hook_gamestart_ttt //LegendGuard adds ttt client hook for TTT 20-02-2021 alias cl_hook_gameend "rpn /cl_matchcount dup load 1 + =" // increase match count every time a game ends alias cl_hook_shutdown alias cl_hook_activeweapon diff --git a/gamemodes-server.cfg b/gamemodes-server.cfg index c15baaf9d..05531496e 100644 --- a/gamemodes-server.cfg +++ b/gamemodes-server.cfg @@ -29,6 +29,7 @@ alias sv_hook_gamestart_ka alias sv_hook_gamestart_ft alias sv_hook_gamestart_inv alias sv_hook_gamestart_duel +alias sv_hook_gamestart_ttt //LegendGuard adds ttt hook for TTT 20-02-2021 // there is currently no hook for when the match is restarted // see sv_hook_readyrestart for previous uses of this hook //alias sv_hook_gamerestart @@ -58,6 +59,7 @@ alias sv_vote_gametype_hook_ons alias sv_vote_gametype_hook_rc alias sv_vote_gametype_hook_tdm alias sv_vote_gametype_hook_duel +alias sv_vote_gametype_hook_ttt //LegendGuard adds ttt hook for TTT 20-02-2021 // Example preset to allow 1v1ctf to be used for the gametype voting screen. // Aliases can have max 31 chars so the gametype can have max 9 chars. @@ -208,6 +210,13 @@ set g_duel_respawn_delay_large_count 0 set g_duel_respawn_delay_max 0 set g_duel_respawn_waves 0 set g_duel_weapon_stay 0 +set g_ttt_respawn_delay_small 0 //LegendGuard adds ttt cvars for TTT 20-02-2021 +set g_ttt_respawn_delay_small_count 0 +set g_ttt_respawn_delay_large 0 +set g_ttt_respawn_delay_large_count 0 +set g_ttt_respawn_delay_max 0 +set g_ttt_respawn_waves 0 +set g_ttt_weapon_stay 0 // ========= @@ -556,3 +565,15 @@ set g_duel 0 "Duel: frag the opponent more in a one versus one arena battle" //set g_duel_warmup 180 "Have a short warmup period before beginning the actual duel" set g_duel_with_powerups 0 "Enable powerups to spawn in the duel gamemode" set g_duel_not_dm_maps 0 "when this is set, DM maps will NOT be listed in duel" + +//LegendGuard adds ttt cvars for TTT 20-02-2021 +// ========== +// trouble in terrorist town +// ========== +set g_ttt 0 "Trouble in Terrorist Town: A group of space terrorists have traitors among them. Traitors must kill terrorists, while the terrorists have to try to find and kill the traitors" +set g_ttt_not_lms_maps 0 "when this is set, LMS maps will NOT be listed in ttt" +set g_ttt_traitor_count 0.25 "number of players who will become traitors, 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" +set g_ttt_punish_teamkill 1 "kill the player when they kill an ally" +set g_ttt_reward_innocent 1 "give a point to all innocent players if the round timelimit is reached, in addition to the points given for kills" +set g_ttt_warmup 10 "how long the players will have time to run around the map before the round starts" +set g_ttt_round_timelimit 180 "round time limit in seconds" \ No newline at end of file diff --git a/gfx/menu/luma/gametype_ttt.tga b/gfx/menu/luma/gametype_ttt.tga new file mode 100644 index 000000000..d36db5ddd Binary files /dev/null and b/gfx/menu/luma/gametype_ttt.tga differ diff --git a/gfx/menu/luminos/gametype_ttt.tga b/gfx/menu/luminos/gametype_ttt.tga new file mode 100644 index 000000000..7fd5a5341 Binary files /dev/null and b/gfx/menu/luminos/gametype_ttt.tga differ diff --git a/gfx/menu/wickedx/gametype_ttt.tga b/gfx/menu/wickedx/gametype_ttt.tga new file mode 100644 index 000000000..7fd5a5341 Binary files /dev/null and b/gfx/menu/wickedx/gametype_ttt.tga differ diff --git a/gfx/menu/xaw/gametype_ttt.tga b/gfx/menu/xaw/gametype_ttt.tga new file mode 100644 index 000000000..f0be4ad8e Binary files /dev/null and b/gfx/menu/xaw/gametype_ttt.tga differ diff --git a/notifications.cfg b/notifications.cfg index 61fafdecb..ff9fbfad7 100644 --- a/notifications.cfg +++ b/notifications.cfg @@ -285,6 +285,11 @@ seta notification_INFO_SUPERSPEC_MISSING_UID "2" "0 = off, 1 = print to console, seta notification_INFO_SUPERWEAPON_PICKUP "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_TEAMCHANGE_LARGERTEAM "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_TEAMCHANGE_NOTALLOWED "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" + +//LegendGuard adds ttt notifications for TTT 20-02-2021 +seta notification_INFO_TTT_TRAITOR_WIN "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" +seta notification_INFO_TTT_INNOCENT_WIN "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" + seta notification_INFO_VERSION_BETA "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_VERSION_OLD "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_VERSION_OUTDATED "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" @@ -538,6 +543,12 @@ seta notification_CENTER_TEAMCHANGE_SPECTATE "1" "0 = off, 1 = centerprint" seta notification_CENTER_TEAMCHANGE_SUICIDE "1" "0 = off, 1 = centerprint" seta notification_CENTER_TIMEOUT_BEGINNING "1" "0 = off, 1 = centerprint" seta notification_CENTER_TIMEOUT_ENDING "1" "0 = off, 1 = centerprint" +//LegendGuard adds ttt notification for TTT 20-02-2021 +seta notification_CENTER_TTT_TRAITOR "1" "0 = off, 1 = centerprint" +seta notification_CENTER_TTT_TRAITOR_WIN "1" "0 = off, 1 = centerprint" +seta notification_CENTER_TTT_INNOCENT "1" "0 = off, 1 = centerprint" +seta notification_CENTER_TTT_INNOCENT_WIN "1" "0 = off, 1 = centerprint" + seta notification_CENTER_VEHICLE_ENTER "1" "0 = off, 1 = centerprint" seta notification_CENTER_VEHICLE_ENTER_GUNNER "1" "0 = off, 1 = centerprint" seta notification_CENTER_VEHICLE_ENTER_STEAL "1" "0 = off, 1 = centerprint" diff --git a/qcsrc/common/ent_cs.qc b/qcsrc/common/ent_cs.qc index 777bd417a..5f0d78897 100644 --- a/qcsrc/common/ent_cs.qc +++ b/qcsrc/common/ent_cs.qc @@ -157,6 +157,12 @@ ENTCS_PROP(SOLID, true, sv_solid, solid, ENTCS_SET_NORMAL, { WriteByte(chan, ent.sv_solid); }, { ent.sv_solid = ReadByte(); }) +//LegendGuard adds ENTCS_PROP from TTT 20-02-2021 +// gamemode specific player ttt status (independent of score and frags) +ENTCS_PROP(TTT_STATUS, true, ttt_status, ttt_status, ENTCS_SET_NORMAL, + { WriteShort(chan, ent.ttt_status); }, + { ent.ttt_status = ReadShort(); }) + #ifdef SVQC int ENTCS_PUBLICMASK = 0; diff --git a/qcsrc/common/gamemodes/gamemode/_mod.inc b/qcsrc/common/gamemodes/gamemode/_mod.inc index a33ec87a0..759f00def 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.inc +++ b/qcsrc/common/gamemodes/gamemode/_mod.inc @@ -15,4 +15,5 @@ #include #include #include -#include +#include //LegendGuard adds _mod.inc for Trouble In Terrorist Town 20-02-2021 +#include \ No newline at end of file diff --git a/qcsrc/common/gamemodes/gamemode/_mod.qh b/qcsrc/common/gamemodes/gamemode/_mod.qh index ffd71d59d..ef725161b 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.qh +++ b/qcsrc/common/gamemodes/gamemode/_mod.qh @@ -15,4 +15,5 @@ #include #include #include -#include +#include //LegendGuard adds _mod.qh for Trouble In Terrorist Town 20-02-2021 +#include \ No newline at end of file diff --git a/qcsrc/common/gamemodes/gamemode/ttt/_mod.inc b/qcsrc/common/gamemodes/gamemode/ttt/_mod.inc new file mode 100644 index 000000000..5574f4aaf --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/ttt/_mod.inc @@ -0,0 +1,8 @@ +// generated file; do not modify +#include +#ifdef CSQC + #include +#endif +#ifdef SVQC + #include +#endif diff --git a/qcsrc/common/gamemodes/gamemode/ttt/_mod.qh b/qcsrc/common/gamemodes/gamemode/ttt/_mod.qh new file mode 100644 index 000000000..09983d725 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/ttt/_mod.qh @@ -0,0 +1,8 @@ +// generated file; do not modify +#include +#ifdef CSQC + #include +#endif +#ifdef SVQC + #include +#endif diff --git a/qcsrc/common/gamemodes/gamemode/ttt/cl_ttt.qc b/qcsrc/common/gamemodes/gamemode/ttt/cl_ttt.qc new file mode 100644 index 000000000..a5b4ce4f0 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/ttt/cl_ttt.qc @@ -0,0 +1,84 @@ +#include "cl_ttt.qh" + +#include +#include + +void HUD_Mod_TTT(vector pos, vector mySize) +{ + mod_active = 1; // ttt should always show the mod HUD + + int mystatus = entcs_receiver(player_localnum).ttt_status; + string player_text = ""; + vector player_color = '1 1 1'; + //string player_icon = ""; + if(mystatus == TTT_STATUS_TRAITOR) + { + player_text = _("Traitor"); + player_color = '1 0 0'; + //player_icon = "player_red"; + } + else if(mystatus == TTT_STATUS_INNOCENT) + { + player_text = _("Innocent"); + player_color = '0 1 0'; + //player_icon = "player_neutral"; + } + else + { + // if the player has no valid status, don't draw anything + return; + } + + string time_text = string_null; + vector timer_color = '1 1 1'; + if(!STAT(GAME_STOPPED) && !warmup_stage && STAT(TTT_ROUNDTIMER) > 0) + { + float timeleft = max(0, STAT(TTT_ROUNDTIMER) - time); + timeleft = ceil(timeleft); + float minutesLeft = floor(timeleft / 60); + time_text = seconds_tostring(timeleft); + if(intermission_time || minutesLeft >= 5 || warmup_stage || STAT(TTT_ROUNDTIMER) == 0) + timer_color = '1 1 1'; //white + else if(minutesLeft >= 1) + timer_color = '1 1 0'; //yellow + else + timer_color = '1 0 0'; //red + } + + //drawpic_aspect_skin(pos, player_icon, vec2(0.5 * mySize.x, mySize.y), '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL); + if(!time_text) + drawstring_aspect(pos, player_text, vec2(mySize.x, mySize.y), player_color, panel_fg_alpha, DRAWFLAG_NORMAL); + else + { + drawstring_aspect(pos, player_text, vec2(0.5 * mySize.x, mySize.y), player_color, panel_fg_alpha, DRAWFLAG_NORMAL); + drawstring_aspect(pos + eX * (0.5 * mySize.x), time_text, vec2(0.5 * mySize.x, mySize.y), timer_color, panel_fg_alpha, DRAWFLAG_NORMAL); + } +} + +REGISTER_MUTATOR(cl_ttt, true); + +MUTATOR_HOOKFUNCTION(cl_ttt, ForcePlayercolors_Skip, CBC_ORDER_LAST) +{ + if(!ISGAMETYPE(TTT)) + return false; + + entity player = M_ARGV(0, entity); + entity e = entcs_receiver(player.entnum - 1); + int innocent_status = ((e) ? e.ttt_status : 0); + int mystatus = entcs_receiver(player_localnum).ttt_status; + + int plcolor = TTT_COLOR_INNOCENT; // default to innocent + //TODO: add Detective 20-02-2021 + if((mystatus == TTT_STATUS_TRAITOR || intermission || STAT(GAME_STOPPED)) && innocent_status == TTT_STATUS_TRAITOR) + plcolor = TTT_COLOR_TRAITOR; + + + player.colormap = 1024 + plcolor; + return true; +} + +MUTATOR_HOOKFUNCTION(cl_ttt, DrawScoreboard_Force) +{ + // show the scoreboard when the round ends, so players can see who the hunter was + return STAT(GAME_STOPPED); +} diff --git a/qcsrc/common/gamemodes/gamemode/ttt/cl_ttt.qh b/qcsrc/common/gamemodes/gamemode/ttt/cl_ttt.qh new file mode 100644 index 000000000..508ddf2ac --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/ttt/cl_ttt.qh @@ -0,0 +1,3 @@ +#pragma once + +void HUD_Mod_TTT(vector pos, vector mySize); diff --git a/qcsrc/common/gamemodes/gamemode/ttt/sv_ttt.qc b/qcsrc/common/gamemodes/gamemode/ttt/sv_ttt.qc new file mode 100644 index 000000000..4730ec26b --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/ttt/sv_ttt.qc @@ -0,0 +1,460 @@ +#include "sv_ttt.qh" + +float autocvar_g_ttt_traitor_count = 0.25; +float autocvar_g_ttt_round_timelimit = 180; +float autocvar_g_ttt_warmup = 10; +bool autocvar_g_ttt_punish_teamkill = true; +bool autocvar_g_ttt_reward_innocent = true; + +//TODO: add Detective and corpse function if a player is DEAD +void ttt_FakeTimeLimit(entity e, float t) +{ + if(!IS_REAL_CLIENT(e)) + return; +#if 0 + msg_entity = e; + WriteByte(MSG_ONE, 3); // svc_updatestat + WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT + if(t < 0) + WriteCoord(MSG_ONE, autocvar_timelimit); + else + WriteCoord(MSG_ONE, (t + 1) / 60); +#else + STAT(TTT_ROUNDTIMER, e) = t; +#endif +} + +void nades_Clear(entity player); + +void Ttt_UpdateScores(bool timed_out) +{ + // give players their hard-earned kills now that the round is over + FOREACH_CLIENT(true, + { + it.totalfrags += it.ttt_validkills; + if(it.ttt_validkills) + GameRules_scoring_add(it, SCORE, it.ttt_validkills); + it.ttt_validkills = 0; + // player survived the round + if(IS_PLAYER(it) && !IS_DEAD(it)) + { + if(autocvar_g_ttt_reward_innocent && timed_out && it.ttt_status == TTT_STATUS_INNOCENT) + GameRules_scoring_add(it, SCORE, 1); // reward innocents who make it to the end of the round time limit + if(it.ttt_status == TTT_STATUS_INNOCENT) + GameRules_scoring_add(it, TTT_RESISTS, 1); + else if(it.ttt_status == TTT_STATUS_TRAITOR) + GameRules_scoring_add(it, TTT_HUNTS, 1); + } + }); +} + +float Ttt_CheckWinner() +{ + if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0) + { + // if the match times out, innocents win too! + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_TTT_INNOCENT_WIN); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_TTT_INNOCENT_WIN); + FOREACH_CLIENT(true, + { + if(IS_PLAYER(it)) + nades_Clear(it); + ttt_FakeTimeLimit(it, -1); + }); + + Ttt_UpdateScores(true); + + allowed_to_spawn = false; + game_stopped = true; + round_handler_Init(5, autocvar_g_ttt_warmup, autocvar_g_ttt_round_timelimit); + return 1; + } + + int innocent_count = 0, traitor_count = 0; + FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), + { + if(it.ttt_status == TTT_STATUS_INNOCENT) + innocent_count++; + else if(it.ttt_status == TTT_STATUS_TRAITOR) + traitor_count++; + }); + if(innocent_count > 0 && traitor_count > 0) + { + return 0; + } + + if(traitor_count > 0) // traitors win + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_TTT_TRAITOR_WIN); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_TTT_TRAITOR_WIN); + } + else if(innocent_count > 0) // innocents win + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_TTT_INNOCENT_WIN); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_TTT_INNOCENT_WIN); + } + else + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED); + } + + Ttt_UpdateScores(false); + + allowed_to_spawn = false; + game_stopped = true; + round_handler_Init(5, autocvar_g_ttt_warmup, autocvar_g_ttt_round_timelimit); + + FOREACH_CLIENT(true, + { + if(IS_PLAYER(it)) + nades_Clear(it); + ttt_FakeTimeLimit(it, -1); + }); + + return 1; +} + +void Ttt_RoundStart() +{ + allowed_to_spawn = boolean(warmup_stage); + int playercount = 0; + FOREACH_CLIENT(true, + { + if(IS_PLAYER(it) && !IS_DEAD(it)) + { + ++playercount; + it.ttt_status = TTT_STATUS_INNOCENT; + } + else + 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! + it.ttt_validkills = 0; + }); + 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 + int total_traitors = 0; + FOREACH_CLIENT_RANDOM(IS_PLAYER(it) && !IS_DEAD(it), + { + if(total_traitors >= traitor_count) + break; + total_traitors++; + it.ttt_status = TTT_STATUS_TRAITOR; + }); + + FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), + { + if(it.ttt_status == TTT_STATUS_INNOCENT) + Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_TTT_INNOCENT); + else if(it.ttt_status == TTT_STATUS_TRAITOR) + Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_TTT_TRAITOR); + + ttt_FakeTimeLimit(it, round_handler_GetEndTime()); + }); +} + +bool Ttt_CheckPlayers() +{ + static int prev_missing_players; + allowed_to_spawn = true; + int playercount = 0; + FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), + { + ++playercount; + }); + if (playercount >= 2) + { + if(prev_missing_players > 0) + Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS); + prev_missing_players = -1; + return true; + } + if(playercount == 0) + { + if(prev_missing_players > 0) + Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS); + prev_missing_players = -1; + return false; + } + // if we get here, only 1 player is missing + if(prev_missing_players != 1) + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_PLAYERS, 1); + prev_missing_players = 1; + } + return false; +} + +bool ttt_isEliminated(entity e) +{ + if(e.caplayer == 1 && (IS_DEAD(e) || e.frags == FRAGS_PLAYER_OUT_OF_GAME)) + return true; + if(e.caplayer == 0.5) + return true; + return false; +} + +void ttt_Initialize() // run at the start of a match, initiates game mode +{ + GameRules_scoring(0, SFL_SORT_PRIO_PRIMARY, 0, { + field(SP_TTT_RESISTS, "resists", 0); + field(SP_TTT_HUNTS, "hunts", SFL_SORT_PRIO_SECONDARY); + }); + + allowed_to_spawn = true; + round_handler_Spawn(Ttt_CheckPlayers, Ttt_CheckWinner, Ttt_RoundStart); + round_handler_Init(5, autocvar_g_ttt_warmup, autocvar_g_ttt_round_timelimit); + EliminatedPlayers_Init(ttt_isEliminated); +} + + +// ============== +// Hook Functions +// ============== + +MUTATOR_HOOKFUNCTION(ttt, ClientObituary) +{ + // in ttt, announcing a frag would tell everyone who the traitor is + entity frag_attacker = M_ARGV(1, entity); + entity frag_target = M_ARGV(2, entity); + if(IS_PLAYER(frag_attacker) && frag_attacker != frag_target) + { + float frag_deathtype = M_ARGV(3, float); + entity wep_ent = M_ARGV(4, entity); + // "team" kill, a point is awarded to the player by default so we must take it away plus an extra one + // unless the player is going to be punished for suicide, in which case just remove one + if(frag_attacker.ttt_status == frag_target.ttt_status) + GiveFrags(frag_attacker, frag_target, ((autocvar_g_ttt_punish_teamkill) ? -1 : -2), frag_deathtype, wep_ent.weaponentity_fld); + } + + M_ARGV(5, bool) = true; // anonymous attacker +} + +MUTATOR_HOOKFUNCTION(ttt, PlayerPreThink) +{ + entity player = M_ARGV(0, entity); + + if(IS_PLAYER(player) || player.caplayer) + { + // update the scoreboard colour display to out the real killer at the end of the round + // running this every frame to avoid cheats + int plcolor = TTT_COLOR_INNOCENT; + if(player.ttt_status == TTT_STATUS_TRAITOR && game_stopped) + plcolor = TTT_COLOR_TRAITOR; + setcolor(player, plcolor); + } +} + +MUTATOR_HOOKFUNCTION(ttt, PlayerSpawn) +{ + entity player = M_ARGV(0, entity); + + player.ttt_status = 0; + player.ttt_validkills = 0; + player.caplayer = 1; + if (!warmup_stage) + eliminatedPlayers.SendFlags |= 1; +} + +MUTATOR_HOOKFUNCTION(ttt, ForbidSpawn) +{ + entity player = M_ARGV(0, entity); + + // spectators / observers that weren't playing can join; they are + // immediately forced to observe in the PutClientInServer hook + // this way they are put in a team and can play in the next round + if (!allowed_to_spawn && player.caplayer) + return true; + return false; +} + +MUTATOR_HOOKFUNCTION(ttt, PutClientInServer) +{ + entity player = M_ARGV(0, entity); + + if (!allowed_to_spawn && IS_PLAYER(player)) // this is true even when player is trying to join + { + TRANSMUTE(Observer, player); + if (CS(player).jointime != time && !player.caplayer) // not when connecting + { + player.caplayer = 0.5; + Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_JOIN_LATE); + } + } +} + +MUTATOR_HOOKFUNCTION(ttt, reset_map_players) +{ + FOREACH_CLIENT(true, { + CS(it).killcount = 0; + it.ttt_status = 0; + ttt_FakeTimeLimit(it, -1); // restore original timelimit + if (!it.caplayer && IS_BOT_CLIENT(it)) + it.caplayer = 1; + if (it.caplayer) + { + TRANSMUTE(Player, it); + it.caplayer = 1; + PutClientInServer(it); + } + }); + bot_relinkplayerlist(); + return true; +} + +MUTATOR_HOOKFUNCTION(ttt, reset_map_global) +{ + allowed_to_spawn = true; + return true; +} + +entity ttt_LastPlayerForTeam(entity this) +{ + entity last_pl = NULL; + FOREACH_CLIENT(IS_PLAYER(it) && it != this, { + if (!IS_DEAD(it) && this.ttt_status == it.ttt_status) + { + if (!last_pl) + last_pl = it; + else + return NULL; + } + }); + return last_pl; +} + +void ttt_LastPlayerForTeam_Notify(entity this) +{ + if (!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted()) + { + entity pl = ttt_LastPlayerForTeam(this); + if (pl) + Send_Notification(NOTIF_ONE_ONLY, pl, MSG_CENTER, CENTER_ALONE); + } +} + +MUTATOR_HOOKFUNCTION(ttt, PlayerDies) +{ + entity frag_attacker = M_ARGV(1, entity); + entity frag_target = M_ARGV(2, entity); + float frag_deathtype = M_ARGV(3, float); + + ttt_LastPlayerForTeam_Notify(frag_target); + if (!allowed_to_spawn) + { + frag_target.respawn_flags = RESPAWN_SILENT; + // prevent unwanted sudden rejoin as spectator and movement of spectator camera + frag_target.respawn_time = time + 2; + } + frag_target.respawn_flags |= RESPAWN_FORCE; + if (!warmup_stage) + { + eliminatedPlayers.SendFlags |= 1; + if (IS_BOT_CLIENT(frag_target)) + bot_clear(frag_target); + } + + // killed an ally! punishment is death + if(autocvar_g_ttt_punish_teamkill && frag_attacker != frag_target && IS_PLAYER(frag_attacker) && IS_PLAYER(frag_target) && frag_attacker.ttt_status == frag_target.ttt_status && !ITEM_DAMAGE_NEEDKILL(frag_deathtype)) + if(!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted()) // don't autokill if the round hasn't + Damage(frag_attacker, frag_attacker, frag_attacker, 100000, DEATH_MIRRORDAMAGE.m_id, DMG_NOWEP, frag_attacker.origin, '0 0 0'); + return true; +} + +MUTATOR_HOOKFUNCTION(ttt, ClientDisconnect) +{ + entity player = M_ARGV(0, entity); + + if (IS_PLAYER(player) && !IS_DEAD(player)) + ttt_LastPlayerForTeam_Notify(player); + return true; +} + +MUTATOR_HOOKFUNCTION(ttt, MakePlayerObserver) +{ + entity player = M_ARGV(0, entity); + + if (IS_PLAYER(player) && !IS_DEAD(player)) + ttt_LastPlayerForTeam_Notify(player); + if (player.killindicator_teamchange == -2) // player wants to spectate + player.caplayer = 0; + if (player.caplayer) + player.frags = FRAGS_PLAYER_OUT_OF_GAME; + if (!warmup_stage) + eliminatedPlayers.SendFlags |= 1; + if (!player.caplayer) + { + player.ttt_validkills = 0; + player.ttt_status = 0; + ttt_FakeTimeLimit(player, -1); // restore original timelimit + return false; // allow team reset + } + return true; // prevent team reset +} + +MUTATOR_HOOKFUNCTION(ttt, Scores_CountFragsRemaining) +{ + // announce remaining frags? + return true; +} + +MUTATOR_HOOKFUNCTION(ttt, GiveFragsForKill, CBC_ORDER_FIRST) +{ + entity frag_attacker = M_ARGV(0, entity); + if(!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted()) + frag_attacker.ttt_validkills += M_ARGV(2, float); + M_ARGV(2, float) = 0; // score will be given to the winner when the round ends + return true; +} + +MUTATOR_HOOKFUNCTION(ttt, AddPlayerScore) +{ + entity scorefield = M_ARGV(0, entity); + if(scorefield == SP_KILLS || scorefield == SP_DEATHS || scorefield == SP_SUICIDES || scorefield == SP_DMG || scorefield == SP_DMGTAKEN) + M_ARGV(1, float) = 0; // don't report that the player has killed or been killed, that would out them as a traitor! +} + +MUTATOR_HOOKFUNCTION(ttt, CalculateRespawnTime) +{ + // no respawn calculations needed, player is forced to spectate anyway + return true; +} + +MUTATOR_HOOKFUNCTION(ttt, Bot_FixCount, CBC_ORDER_EXCLUSIVE) +{ + FOREACH_CLIENT(IS_REAL_CLIENT(it), { + if (IS_PLAYER(it) || it.caplayer == 1) + ++M_ARGV(0, int); + ++M_ARGV(1, int); + }); + return true; +} + +MUTATOR_HOOKFUNCTION(ttt, ClientCommand_Spectate) +{ + entity player = M_ARGV(0, entity); + + if (player.caplayer) + { + // they're going to spec, we can do other checks + if (autocvar_sv_spectate && (IS_SPEC(player) || IS_OBSERVER(player))) + Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_LEAVE); + return MUT_SPECCMD_FORCE; + } + + return MUT_SPECCMD_CONTINUE; +} + +MUTATOR_HOOKFUNCTION(ttt, GetPlayerStatus) +{ + entity player = M_ARGV(0, entity); + + return player.caplayer == 1; +} + +MUTATOR_HOOKFUNCTION(ttt, BotShouldAttack) +{ + entity bot = M_ARGV(0, entity); + entity targ = M_ARGV(1, entity); + + if(targ.ttt_status == bot.ttt_status) + return true; +} diff --git a/qcsrc/common/gamemodes/gamemode/ttt/sv_ttt.qh b/qcsrc/common/gamemodes/gamemode/ttt/sv_ttt.qh new file mode 100644 index 000000000..20b300bfc --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/ttt/sv_ttt.qh @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +void ttt_Initialize(); + +REGISTER_MUTATOR(ttt, false) +{ + MUTATOR_STATIC(); + MUTATOR_ONADD + { + ttt_Initialize(); + } + return false; +} + +.int ttt_validkills; // store the player's valid kills to be given at the end of the match (avoid exposing their score until then) diff --git a/qcsrc/common/gamemodes/gamemode/ttt/ttt.qc b/qcsrc/common/gamemodes/gamemode/ttt/ttt.qc new file mode 100644 index 000000000..0e43e0925 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/ttt/ttt.qc @@ -0,0 +1 @@ +#include "ttt.qh" diff --git a/qcsrc/common/gamemodes/gamemode/ttt/ttt.qh b/qcsrc/common/gamemodes/gamemode/ttt/ttt.qh new file mode 100644 index 000000000..d2bc4ff6e --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/ttt/ttt.qh @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +#ifdef CSQC +void HUD_Mod_TTT(vector pos, vector mySize); +#endif +CLASS(TroubleinTerroristTown, Gametype) + INIT(TroubleinTerroristTown) + { + this.gametype_init(this, _("Trouble in Terrorist Town"),"ttt","g_ttt",GAMETYPE_FLAG_USEPOINTS,"","timelimit=30 pointlimit=20",_("A group of space terrorists have traitors among them. Traitors must kill terrorists, while the terrorists have to try to find and kill the traitors")); + } + METHOD(TroubleinTerroristTown, m_isAlwaysSupported, bool(Gametype this, int spawnpoints, float diameter)) + { + return true; + } + METHOD(TroubleinTerroristTown, m_isForcedSupported, bool(Gametype this)) + { + if(!cvar("g_ttt_not_lms_maps")) + { + // if this is unset, all LMS maps support TroubleinTerroristTown too + if(!(MapInfo_Map_supportedGametypes & this.m_flags) && (MapInfo_Map_supportedGametypes & MAPINFO_TYPE_LMS.m_flags)) + return true; // TODO: references another gametype (alternatively, we could check which gamemodes are always enabled and append this if any are supported) + } + return false; + } +#ifdef CSQC + ATTRIB(TroubleinTerroristTown, m_modicons, void(vector pos, vector mySize), HUD_Mod_TTT); +#endif +ENDCLASS(TroubleinTerroristTown) +REGISTER_GAMETYPE(TTT, NEW(TroubleinTerroristTown)); + +#ifdef GAMEQC +// shared state signalling the player's ttt status +//TODO: add Detective STATUS and COLOR 20-02-2021 +.int ttt_status; +const int TTT_STATUS_INNOCENT = 1; +const int TTT_STATUS_TRAITOR = 2; + +// hardcoded player colors for ttt +const int TTT_COLOR_INNOCENT = 51; // green +const int TTT_COLOR_TRAITOR = 68; // red +#endif diff --git a/qcsrc/common/notifications/all.inc b/qcsrc/common/notifications/all.inc index d692d73c8..0e4bb7157 100644 --- a/qcsrc/common/notifications/all.inc +++ b/qcsrc/common/notifications/all.inc @@ -439,6 +439,9 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_INFO_NOTIF(TEAMCHANGE_LARGERTEAM, N_CONSOLE, 0, 0, "", "", "", _("^BGYou cannot change to a larger team"), "") MSG_INFO_NOTIF(TEAMCHANGE_NOTALLOWED, N_CONSOLE, 0, 0, "", "", "", _("^BGYou are not allowed to change teams"), "") + //LegendGuard adds MSG_INFO_NOTIF for TTT 20-02-2021 + MSG_INFO_NOTIF(TTT_TRAITOR_WIN, N_CONSOLE, 0, 0, "", "", "", _("^K1Traitors^BG win the round"), "") + MSG_INFO_NOTIF(TTT_INNOCENT_WIN, N_CONSOLE, 0, 0, "", "", "", _("^F1Innocents^BG win the round"), "") MSG_INFO_NOTIF(VERSION_BETA, N_CONSOLE, 2, 0, "s1 s2", "", "", _("^F4NOTE: ^BGThe server is running ^F1Xonotic %s (beta)^BG, you have ^F2Xonotic %s"), "") MSG_INFO_NOTIF(VERSION_OLD, N_CHATCON, 2, 0, "s1 s2", "", "", _("^F4NOTE: ^BGThe server is running ^F1Xonotic %s^BG, you have ^F2Xonotic %s"), "") @@ -770,6 +773,11 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_CENTER_NOTIF(TIMEOUT_BEGINNING, N_ENABLE, 0, 1, "", CPID_TIMEOUT, "1 f1", _("^F4Timeout begins in ^COUNT"), "") MSG_CENTER_NOTIF(TIMEOUT_ENDING, N_ENABLE, 0, 1, "", CPID_TIMEIN, "1 f1", _("^F4Timeout ends in ^COUNT"), "") + //LegendGuard adds MSG_CENTER_NOTIF for TTT 20-02-2021 + MSG_CENTER_NOTIF(TTT_TRAITOR, N_ENABLE, 0, 0, "", CPID_TTT, "5 0", strcat(BOLD_OPERATOR, _("^BGYou are ^K1traitor^BG! Kill all the innocents without raising suspicion!")), "") + MSG_CENTER_NOTIF(TTT_TRAITOR_WIN, N_ENABLE, 0, 0, "", CPID_ROUND, "0 0", _("^K1Traitors^BG win the round"), "") + MSG_CENTER_NOTIF(TTT_INNOCENT, N_ENABLE, 0, 0, "", CPID_TTT, "5 0", strcat(BOLD_OPERATOR, _("^BGYou are ^F1innocent^BG! Try to find out who is/are traitor(s) and survive until time is up!")), "") + MSG_CENTER_NOTIF(TTT_INNOCENT_WIN, N_ENABLE, 0, 0, "", CPID_ROUND, "0 0", _("^F1Innocents^BG win the round"), "") MSG_CENTER_NOTIF(JOIN_PREVENT_MINIGAME, N_ENABLE, 0, 0, "", CPID_Null, "0 0", _("^K1Cannot join given minigame session!"), "" ) diff --git a/qcsrc/common/notifications/all.qh b/qcsrc/common/notifications/all.qh index 72017e3dc..8c921055a 100644 --- a/qcsrc/common/notifications/all.qh +++ b/qcsrc/common/notifications/all.qh @@ -40,7 +40,7 @@ string Get_Notif_TypeName(MSG net_type) LOG_WARNF("Get_Notif_TypeName(%d): Improper net type!", ORDINAL(net_type)); return ""; } - +//LegendGuard adds CASE(CPID, TTT) after TIMEIN for TTT 20-02-2021 ENUMCLASS(CPID) CASE(CPID, ASSAULT_ROLE) CASE(CPID, ROUND) @@ -72,6 +72,7 @@ ENUMCLASS(CPID) CASE(CPID, TEAMCHANGE) CASE(CPID, TIMEOUT) CASE(CPID, TIMEIN) + CASE(CPID, TTT) CASE(CPID, VEHICLES) CASE(CPID, VEHICLES_OTHER) /** always last */ diff --git a/qcsrc/common/scores.qh b/qcsrc/common/scores.qh index 3bc6c5563..84d992ac8 100644 --- a/qcsrc/common/scores.qh +++ b/qcsrc/common/scores.qh @@ -85,6 +85,10 @@ REGISTER_SP(NEXBALL_FAULTS); REGISTER_SP(ONS_TAKES); REGISTER_SP(ONS_CAPS); + +REGISTER_SP(TTT_RESISTS); //LegendGuard adds REGISTER_SP for TTT 20-02-2021 //Innocents as number of suvivals +REGISTER_SP(TTT_HUNTS); //Traitors as number of hunts + #endif diff --git a/qcsrc/common/stats.qh b/qcsrc/common/stats.qh index 67f7fd60c..880b378c8 100644 --- a/qcsrc/common/stats.qh +++ b/qcsrc/common/stats.qh @@ -434,3 +434,5 @@ REGISTER_STAT(GUNALIGN, int) #ifdef SVQC SPECTATE_COPYFIELD(_STAT(GUNALIGN)) #endif + +REGISTER_STAT(TTT_ROUNDTIMER, float) //LegendGuard adds TTT_ROUNDTIMER for TTT 20-02-2021 diff --git a/qcsrc/menu/xonotic/util.qc b/qcsrc/menu/xonotic/util.qc index e77049d20..5a98716bd 100644 --- a/qcsrc/menu/xonotic/util.qc +++ b/qcsrc/menu/xonotic/util.qc @@ -683,6 +683,8 @@ float updateCompression() GAMETYPE(MAPINFO_TYPE_ASSAULT) \ /* GAMETYPE(MAPINFO_TYPE_DUEL) */ \ /* GAMETYPE(MAPINFO_TYPE_INVASION) */ \ + GAMETYPE(MAPINFO_TYPE_TTT) \ + //LegendGuard adds GAMETYPE for menu for TTT 20-02-2021 /**/ // hidden gametypes come last so indexing always works correctly diff --git a/qcsrc/server/world.qc b/qcsrc/server/world.qc index 9da86388c..91cb13810 100644 --- a/qcsrc/server/world.qc +++ b/qcsrc/server/world.qc @@ -301,6 +301,8 @@ void cvar_changes_init() BADCVAR("g_tdm"); BADCVAR("g_tdm_on_dm_maps"); BADCVAR("g_tdm_teams"); + BADCVAR("g_ttt"); + BADCVAR("g_ttt_not_dm_maps"); BADCVAR("g_vip"); BADCVAR("leadlimit"); BADCVAR("nextmap"); @@ -310,6 +312,7 @@ void cvar_changes_init() BADCVAR("g_mapinfo_ignore_warnings"); BADCVAR("g_maplist_ignore_sizes"); BADCVAR("g_maplist_sizes_count_bots"); + //LegendGuard adds BADCVAR(g_*) for TTT 20-02-2021 // long BADCVAR("hostname");