From cf1c7c4ba6e9a76b83402821fc473ed09f89d4af Mon Sep 17 00:00:00 2001 From: squeaktoy Date: Wed, 10 Jul 2024 20:16:53 +0200 Subject: [PATCH] add Infection gamemode: survivors must avoid getting infected The game begins with only survivors. After a few seconds, a random player will be selected to become infected. If a survivor is killed by an infected player, the survivor will become infected. If all survivors are dead, the infected win. If the infected fail to kill all survivors within the timeframe, survivors win. This commit is incomplete and will not work. Current issues: - Fix game crash when a survivor is killed - Mutators for setting what weapons infected have at their disposal --- gamemodes-client.cfg | 1 + gamemodes-server.cfg | 12 ++ notifications.cfg | 2 + qcsrc/common/gamemodes/gamemode/_mod.inc | 1 + qcsrc/common/gamemodes/gamemode/_mod.qh | 1 + .../gamemodes/gamemode/infection/_mod.inc | 5 + .../gamemodes/gamemode/infection/_mod.qh | 5 + .../gamemodes/gamemode/infection/infection.qc | 1 + .../gamemodes/gamemode/infection/infection.qh | 30 +++ .../gamemode/infection/sv_infection.qc | 172 ++++++++++++++++++ .../gamemode/infection/sv_infection.qh | 14 ++ qcsrc/common/notifications/all.inc | 8 + qcsrc/menu/xonotic/util.qc | 1 + qcsrc/server/world.qc | 1 + 14 files changed, 254 insertions(+) create mode 100644 qcsrc/common/gamemodes/gamemode/infection/_mod.inc create mode 100644 qcsrc/common/gamemodes/gamemode/infection/_mod.qh create mode 100644 qcsrc/common/gamemodes/gamemode/infection/infection.qc create mode 100644 qcsrc/common/gamemodes/gamemode/infection/infection.qh create mode 100644 qcsrc/common/gamemodes/gamemode/infection/sv_infection.qc create mode 100644 qcsrc/common/gamemodes/gamemode/infection/sv_infection.qh diff --git a/gamemodes-client.cfg b/gamemodes-client.cfg index c63bba5a7..4dc3aa570 100644 --- a/gamemodes-client.cfg +++ b/gamemodes-client.cfg @@ -30,6 +30,7 @@ alias cl_hook_gamestart_nb alias cl_hook_gamestart_cts alias cl_hook_gamestart_ka alias cl_hook_gamestart_ft +alias cl_hook_gamestart_infect alias cl_hook_gamestart_inv alias cl_hook_gamestart_duel alias cl_hook_gamestart_mayhem diff --git a/gamemodes-server.cfg b/gamemodes-server.cfg index f3b6a95bb..6bbe3adbe 100644 --- a/gamemodes-server.cfg +++ b/gamemodes-server.cfg @@ -27,6 +27,7 @@ alias sv_hook_gamestart_nb alias sv_hook_gamestart_cts alias sv_hook_gamestart_ka alias sv_hook_gamestart_ft +alias sv_hook_gamestart_infect alias sv_hook_gamestart_inv alias sv_hook_gamestart_duel alias sv_hook_gamestart_mayhem @@ -53,6 +54,7 @@ alias sv_vote_gametype_hook_cts alias sv_vote_gametype_hook_dm alias sv_vote_gametype_hook_dom alias sv_vote_gametype_hook_ft +alias sv_vote_gametype_hook_infection alias sv_vote_gametype_hook_inv alias sv_vote_gametype_hook_ka alias sv_vote_gametype_hook_kh @@ -596,6 +598,16 @@ set g_race_qualifying_timelimit_override -1 "qualifying session time limit overr set g_race_teams 0 "when 2, 3, or 4, the race is played as a team game (the team members can add up their laps)" set g_race_cptimes_onlyself 0 "only show player's own checkpoint times" +// =========== +// infection +// =========== + +set g_infection 0 "Infection: survivors must avoid getting infected" +set g_infection_round_timelimit 900 "maximum time for the infected to infect all survivors" +set g_infection_warmup "how long the players will have time to run around the map before the round starts" +set g_infection_infected_count "the minimum amount of players to infect" +set g_infection_round_enddelay "seconds of delay for score evaluation after round could end" + // ========== // invasion // ========== diff --git a/notifications.cfg b/notifications.cfg index 92f416971..c5ea8eb2a 100644 --- a/notifications.cfg +++ b/notifications.cfg @@ -287,6 +287,8 @@ 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_SURVIVAL_HUNTER_WIN "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_SURVIVAL_SURVIVOR_WIN "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" +seta notification_INFO_INFECTION_INFECTED_WIN "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" +seta notification_INFO_INFECTION_SURVIVOR_WIN "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)" seta notification_INFO_VERSION_BETA "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" diff --git a/qcsrc/common/gamemodes/gamemode/_mod.inc b/qcsrc/common/gamemodes/gamemode/_mod.inc index 75b1ea001..7eec50161 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.inc +++ b/qcsrc/common/gamemodes/gamemode/_mod.inc @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include diff --git a/qcsrc/common/gamemodes/gamemode/_mod.qh b/qcsrc/common/gamemodes/gamemode/_mod.qh index 776a88d25..938ad192a 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.qh +++ b/qcsrc/common/gamemodes/gamemode/_mod.qh @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include diff --git a/qcsrc/common/gamemodes/gamemode/infection/_mod.inc b/qcsrc/common/gamemodes/gamemode/infection/_mod.inc new file mode 100644 index 000000000..5393d746e --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/infection/_mod.inc @@ -0,0 +1,5 @@ +// generated file; do not modify +#include +#ifdef SVQC + #include +#endif diff --git a/qcsrc/common/gamemodes/gamemode/infection/_mod.qh b/qcsrc/common/gamemodes/gamemode/infection/_mod.qh new file mode 100644 index 000000000..a9ce57b4b --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/infection/_mod.qh @@ -0,0 +1,5 @@ +// generated file; do not modify +#include +#ifdef SVQC + #include +#endif diff --git a/qcsrc/common/gamemodes/gamemode/infection/infection.qc b/qcsrc/common/gamemodes/gamemode/infection/infection.qc new file mode 100644 index 000000000..0b8a13acd --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/infection/infection.qc @@ -0,0 +1 @@ +#include "infection.qh" diff --git a/qcsrc/common/gamemodes/gamemode/infection/infection.qh b/qcsrc/common/gamemodes/gamemode/infection/infection.qh new file mode 100644 index 000000000..5ae1f6067 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/infection/infection.qh @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +CLASS(Infection, Gametype) + INIT(Infection) + { + this.gametype_init(this, _("Infection"),"inf","g_infection",GAMETYPE_FLAG_TEAMPLAY,"","timelimit=30",_("Survivors must avoid getting infected")); + } + METHOD(Infection, m_isAlwaysSupported, bool(Gametype this, int spawnpoints, float diameter)) + { + return true; + } + METHOD(Infection, m_isForcedSupported, bool(Gametype this)) + { + if(!(MapInfo_Map_supportedGametypes & this.gametype_flags) && (MapInfo_Map_supportedGametypes & MAPINFO_TYPE_DEATHMATCH.gametype_flags)) + { + return true; + } + if(!(MapInfo_Map_supportedGametypes & this.gametype_flags) && (MapInfo_Map_supportedGametypes & MAPINFO_TYPE_TEAM_DEATHMATCH.gametype_flags)) + { + return true; + } + return false; + } +ENDCLASS(Infection) +REGISTER_GAMETYPE(INFECTION, NEW(Infection)); +#define g_infection IS_GAMETYPE(TEAM_DEATHMATCH) diff --git a/qcsrc/common/gamemodes/gamemode/infection/sv_infection.qc b/qcsrc/common/gamemodes/gamemode/infection/sv_infection.qc new file mode 100644 index 000000000..30a2ab139 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/infection/sv_infection.qc @@ -0,0 +1,172 @@ +#include "sv_infection.qh" + +const int NUM_TEAM_SURVIVOR = 2; +const int NUM_TEAM_INFECTED = 1; + +float autocvar_g_infection_round_timelimit = 900; +float autocvar_g_infection_warmup = 10; +float autocvar_g_infection_infected_count = 1; +float autocvar_g_infection_round_enddelay = 1; + +spawnfunc(inf_team) +{ + if(!g_infection || !this.cnt) { delete(this); return; } + + this.team = this.cnt + 1; +} + +// code from here on is just to support maps that don't have team entities +void Infection_SpawnTeam (string teamname, int teamcolor) +{ + entity this = new_pure(inf_team); + this.netname = teamname; + this.cnt = teamcolor - 1; + this.team = teamcolor; + this.spawnfunc_checked = true; +} + +void Infection_InitTeams(entity this) +{ + // if no teams are found, spawn defaults + if(find(NULL, classname, "Infection_team") == NULL) + { + LOG_TRACE("No \"infection_team\" entities found on this map, creating them anyway."); + + int numteams = 2; + + int teams = BITS(numteams); + if(teams & BIT(0)) + Infection_SpawnTeam("Infected", NUM_TEAM_2); + if(teams & BIT(1)) + Infection_SpawnTeam("Survivor", NUM_TEAM_1); + } +} + +bool Infection_CheckPlayers() +{ + allowed_to_spawn = true; + int playercount = 0; + FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), + { + ++playercount; + }); + if (playercount < 2) { + Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS); + return false; + } + return true; +} + +bool Infection_CheckWinner() +{ + if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0 + && autocvar_g_infection_round_enddelay == -1) + { + // if the match times out, survivors win + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_INFECTION_SURVIVOR_WIN); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_INFECTION_SURVIVOR_WIN); + + allowed_to_spawn = false; + game_stopped = true; + round_handler_Init(5, autocvar_g_infection_warmup, autocvar_g_infection_round_timelimit); + return 1; + } + + int survivor_count = 0, infected_count = 0; + FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), + { + if(Entity_GetTeamIndex(it) == NUM_TEAM_SURVIVOR) + ++survivor_count; + else if(Entity_GetTeamIndex(it) == NUM_TEAM_INFECTED) + ++infected_count; + }); + // Not enough infected? Let's infect some players. + int playercount = survivor_count + infected_count; + int must_infect = bound(1, ((autocvar_g_infection_infected_count >= 1) ? autocvar_g_infection_infected_count : floor(playercount * autocvar_g_infection_infected_count)), playercount - 1); // 20%, but ensure at least 1 and less than total + FOREACH_CLIENT_RANDOM(IS_PLAYER(it) && !IS_DEAD(it), + { + if(infected_count >= must_infect) + break; + ++infected_count; + Player_SetTeamIndex(it, NUM_TEAM_INFECTED); + }); + + if (survivor_count > 0) { + round_handler_ResetEndDelayTime(); + return 0; + } + + // delay round ending a bit + if (autocvar_g_infection_round_enddelay + && round_handler_GetEndTime() > 0 + && round_handler_GetEndTime() - time > 0) // don't delay past timelimit + { + if (round_handler_GetEndDelayTime() == -1) + { + round_handler_SetEndDelayTime(min(time + autocvar_g_infection_round_enddelay, round_handler_GetEndTime())); + return 0; + } + else if (round_handler_GetEndDelayTime() >= time) + { + return 0; + } + } + + if(infected_count > 0) // infected win + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_INFECTION_INFECTED_WIN); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_INFECTION_INFECTED_WIN); + } else { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED); + } + + allowed_to_spawn = false; + game_stopped = true; + round_handler_Init(5, autocvar_g_infection_warmup, autocvar_g_infection_round_timelimit); + return 1; +} + +void Infection_RoundStart() +{ + allowed_to_spawn = boolean(warmup_stage); + FOREACH_CLIENT(IS_PLAYER(it), { + it.player_blocked = false; + Player_SetTeamIndex(it, NUM_TEAM_SURVIVOR); + }); +} + +void Infection_Initialize() +{ + GameRules_teams(true); + GameRules_spawning_teams(2); + InitializeEntity(NULL, Infection_InitTeams, INITPRIO_GAMETYPE); + + allowed_to_spawn = true; + round_handler_Spawn(Infection_CheckPlayers, Infection_CheckWinner, Infection_RoundStart); + round_handler_Init(5, autocvar_g_infection_warmup, autocvar_g_infection_round_timelimit); +} + +MUTATOR_HOOKFUNCTION(inf, TeamBalance_CheckAllowedTeams) +{ + M_ARGV(0, float) = BIT(0); + return true; +} + +MUTATOR_HOOKFUNCTION(inf, PlayerDies) +{ + entity attacker = M_ARGV(1, entity); + entity target = M_ARGV(2, entity); + + if (!IS_PLAYER(attacker)) { + Player_SetTeamIndex(target, NUM_TEAM_INFECTED); + return true; + } + + int attacker_team = Entity_GetTeamIndex(attacker); + + if (DIFF_TEAM(attacker, target) && attacker_team != NUM_TEAM_SURVIVOR) + Player_SetTeamIndex(target, attacker_team); + + return true; +} diff --git a/qcsrc/common/gamemodes/gamemode/infection/sv_infection.qh b/qcsrc/common/gamemodes/gamemode/infection/sv_infection.qh new file mode 100644 index 000000000..fe38ee387 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/infection/sv_infection.qh @@ -0,0 +1,14 @@ +#pragma once + +#include +void Infection_Initialize(); + +REGISTER_MUTATOR(inf, false) +{ + MUTATOR_STATIC(); + MUTATOR_ONADD + { + Infection_Initialize(); + } + return false; +} diff --git a/qcsrc/common/notifications/all.inc b/qcsrc/common/notifications/all.inc index 365f6cb60..cfc682313 100644 --- a/qcsrc/common/notifications/all.inc +++ b/qcsrc/common/notifications/all.inc @@ -455,6 +455,9 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_INFO_NOTIF(SURVIVAL_HUNTER_WIN, N_CONSOLE, 0, 0, "", "", "", _("^K1Hunters^BG win the round"), "") MSG_INFO_NOTIF(SURVIVAL_SURVIVOR_WIN, N_CONSOLE, 0, 0, "", "", "", _("^F1Survivors^BG win the round"), "") + MSG_INFO_NOTIF(INFECTION_INFECTED_WIN, N_CONSOLE, 0, 0, "", "", "", _("^K1Infected^BG wins the round"), "") + MSG_INFO_NOTIF(INFECTION_SURVIVOR_WIN, N_CONSOLE, 0, 0, "", "", "", _("^F1Survivors^BG win the round"), "") + 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"), "") @@ -800,6 +803,11 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_CENTER_NOTIF(SURVIVAL_SURVIVOR, N_ENABLE, 0, 0, "", CPID_SURVIVAL, "5 0", BOLD(_("^BGYou are a ^F1survivor^BG! Identify and eliminate the hunter(s)!")), "") MSG_CENTER_NOTIF(SURVIVAL_SURVIVOR_WIN, N_ENABLE, 0, 0, "", CPID_ROUND, "0 0", _("^F1Survivors^BG win the round"), "") + MSG_CENTER_NOTIF(INFECTION_INFECTED, N_ENABLE, 0, 0, "", CPID_SURVIVAL, "5 0", BOLD(_("^BGYou are ^K1infected^BG! Eliminate the survivor(s)!")), "") + MSG_CENTER_NOTIF(INFECTION_INFECTED_WIN, N_ENABLE, 0, 0, "", CPID_ROUND, "0 0", _("^K1Infected^BG wins the round"), "") + MSG_CENTER_NOTIF(INFECTION_SURVIVOR, N_ENABLE, 0, 0, "", CPID_SURVIVAL, "5 0", BOLD(_("^BGYou are a ^F1survivor^BG! Don't get infected!")), "") + MSG_CENTER_NOTIF(INFECTION_SURVIVOR_WIN, N_ENABLE, 0, 0, "", CPID_ROUND, "0 0", _("^F1Survivors^BG win the round"), "") + MULTITEAM_CENTER(TEAMCHANGE, N_ENABLE, 0, 1, "", CPID_TEAMCHANGE, "1 f1", _("^K1Changing to ^TC^TT^K1 in ^COUNT"), "", NAME) MSG_CENTER_NOTIF(TEAMCHANGE_AUTO, N_ENABLE, 0, 1, "", CPID_TEAMCHANGE, "1 f1", _("^K1Changing team in ^COUNT"), "") MSG_CENTER_NOTIF(TEAMCHANGE_SPECTATE, N_ENABLE, 0, 1, "", CPID_TEAMCHANGE, "1 f1", _("^K1Spectating in ^COUNT"), "") diff --git a/qcsrc/menu/xonotic/util.qc b/qcsrc/menu/xonotic/util.qc index ac3c798a9..3be8e6837 100644 --- a/qcsrc/menu/xonotic/util.qc +++ b/qcsrc/menu/xonotic/util.qc @@ -673,6 +673,7 @@ float updateCompression() GAMETYPE(MAPINFO_TYPE_ONSLAUGHT) \ GAMETYPE(MAPINFO_TYPE_ASSAULT) \ GAMETYPE(MAPINFO_TYPE_SURVIVAL) \ + GAMETYPE(MAPINFO_TYPE_INFECTION) \ /* GAMETYPE(MAPINFO_TYPE_DUEL) */ \ /**/ diff --git a/qcsrc/server/world.qc b/qcsrc/server/world.qc index f051eea48..e86d172cd 100644 --- a/qcsrc/server/world.qc +++ b/qcsrc/server/world.qc @@ -309,6 +309,7 @@ void cvar_changes_init() BADCVAR("g_tmayhem"); BADCVAR("g_tmayhem_teams"); BADCVAR("g_vip"); + BADCVAR("g_infection"); BADCVAR("leadlimit"); BADCVAR("nextmap"); BADCVAR("teamplay"); -- 2.39.2