From: Mario Date: Tue, 9 May 2023 09:12:52 +0000 (+0000) Subject: Add Survival, a gamemode where randomly selected hunters must frag all the survivors... X-Git-Tag: xonotic-v0.8.6~56^2~8^2 X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=55c19e57538b8309b80e536bec1e5e0e7953b616;p=xonotic%2Fxonotic-data.pk3dir.git Add Survival, a gamemode where randomly selected hunters must frag all the survivors without being caught --- diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8ba5c1806..edb153ac4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -75,7 +75,7 @@ test_sv_game: - wget -nv -O data/maps/stormkeep.waypoints https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints - wget -nv -O data/maps/stormkeep.waypoints.cache https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints.cache - - EXPECT=fe5dec36cb304c55acee73afd1e09c0a + - EXPECT=2e2290a8780a78ea6238a7b3d1386e39 - HASH=$(${ENGINE} +timestamps 1 +exec serverbench.cfg | tee /dev/stderr | sed -e 's,^\[[^]]*\] ,,' diff --git a/gamemodes-client.cfg b/gamemodes-client.cfg index 259c2c31d..c0562cfb8 100644 --- a/gamemodes-client.cfg +++ b/gamemodes-client.cfg @@ -34,6 +34,7 @@ alias cl_hook_gamestart_inv alias cl_hook_gamestart_duel alias cl_hook_gamestart_mayhem alias cl_hook_gamestart_tmayhem +alias cl_hook_gamestart_surv alias cl_hook_gameend alias cl_hook_shutdown alias cl_hook_activeweapon diff --git a/gamemodes-server.cfg b/gamemodes-server.cfg index eb0dcdcdc..23c97924e 100644 --- a/gamemodes-server.cfg +++ b/gamemodes-server.cfg @@ -31,6 +31,7 @@ alias sv_hook_gamestart_inv alias sv_hook_gamestart_duel alias sv_hook_gamestart_mayhem alias sv_hook_gamestart_tmayhem +alias sv_hook_gamestart_surv // 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 @@ -62,6 +63,7 @@ alias sv_vote_gametype_hook_tdm alias sv_vote_gametype_hook_duel alias sv_vote_gametype_hook_mayhem alias sv_vote_gametype_hook_tmayhem +alias sv_vote_gametype_hook_surv // 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. @@ -226,6 +228,13 @@ set g_tmayhem_respawn_delay_large_count 0 set g_tmayhem_respawn_delay_max 0 set g_tmayhem_respawn_waves 0 set g_tmayhem_weapon_stay 0 +set g_surv_respawn_delay_small 0 +set g_surv_respawn_delay_small_count 0 +set g_surv_respawn_delay_large 0 +set g_surv_respawn_delay_large_count 0 +set g_surv_respawn_delay_max 0 +set g_surv_respawn_waves 0 +set g_surv_weapon_stay 0 // ========= @@ -636,3 +645,14 @@ set g_tmayhem_rot 0 "health and/or armor rotting, according to g_balance_health_ set g_tmayhem_teams 2 "how many teams are in team mayhem (set by mapinfo)" set g_tmayhem_team_spawns 0 "when 1, players spawn from the team spawnpoints of the map, if any" set g_tmayhem_teams_override 0 "how many teams are in team mayhem" + +// ========== +// survival +// ========== +set g_survival 0 "Survival: identify and eliminate all the hunters before all your allies are gone" +set g_survival_not_lms_maps 0 "when this is set, LMS maps will NOT be listed in survival" +set g_survival_hunter_count 0.25 "number of players who will become hunters, 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_survival_punish_teamkill 1 "kill the player when they kill an ally" +set g_survival_reward_survival 1 "give a point to all surviving players if the round timelimit is reached, in addition to the points given for kills" +set g_survival_warmup 10 "how long the players will have time to run around the map before the round starts" +set g_survival_round_timelimit 120 "round time limit in seconds" diff --git a/gfx/menu/luma/gametype_sv.tga b/gfx/menu/luma/gametype_sv.tga new file mode 100644 index 000000000..050fb7b06 Binary files /dev/null and b/gfx/menu/luma/gametype_sv.tga differ diff --git a/gfx/menu/luminos/gametype_sv.tga b/gfx/menu/luminos/gametype_sv.tga new file mode 100644 index 000000000..c5486651c Binary files /dev/null and b/gfx/menu/luminos/gametype_sv.tga differ diff --git a/gfx/menu/wickedx/gametype_sv.tga b/gfx/menu/wickedx/gametype_sv.tga new file mode 100644 index 000000000..c5486651c Binary files /dev/null and b/gfx/menu/wickedx/gametype_sv.tga differ diff --git a/gfx/menu/xaw/gametype_sv.tga b/gfx/menu/xaw/gametype_sv.tga new file mode 100644 index 000000000..debbce06f Binary files /dev/null and b/gfx/menu/xaw/gametype_sv.tga differ diff --git a/notifications.cfg b/notifications.cfg index 8eff36f2e..c2225f26e 100644 --- a/notifications.cfg +++ b/notifications.cfg @@ -276,6 +276,8 @@ seta notification_INFO_SCORES "1" "0 = off, 1 = print to console, 2 = print to c seta notification_INFO_SPECTATE_WARNING "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_SUPERSPEC_MISSING_UID "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" 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_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)" @@ -533,6 +535,10 @@ seta notification_CENTER_SEQUENCE_COUNTER_FEWMORE "1" "0 = off, 1 = centerprint" seta notification_CENTER_SUPERWEAPON_BROKEN "1" "0 = off, 1 = centerprint" seta notification_CENTER_SUPERWEAPON_LOST "1" "0 = off, 1 = centerprint" seta notification_CENTER_SUPERWEAPON_PICKUP "1" "0 = off, 1 = centerprint" +seta notification_CENTER_SURVIVAL_HUNTER "1" "0 = off, 1 = centerprint" +seta notification_CENTER_SURVIVAL_HUNTER_WIN "1" "0 = off, 1 = centerprint" +seta notification_CENTER_SURVIVAL_SURVIVOR "1" "0 = off, 1 = centerprint" +seta notification_CENTER_SURVIVAL_SURVIVOR_WIN "1" "0 = off, 1 = centerprint" seta notification_CENTER_TEAMCHANGE_AUTO "1" "0 = off, 1 = centerprint" seta notification_CENTER_TEAMCHANGE "1" "0 = off, 1 = centerprint" seta notification_CENTER_TEAMCHANGE_SPECTATE "1" "0 = off, 1 = centerprint" diff --git a/qcsrc/common/ent_cs.qc b/qcsrc/common/ent_cs.qc index 85119de08..b84a98ca6 100644 --- a/qcsrc/common/ent_cs.qc +++ b/qcsrc/common/ent_cs.qc @@ -157,6 +157,11 @@ ENTCS_PROP(SOLID, true, sv_solid, solid, ENTCS_SET_NORMAL, { WriteByte(chan, ent.sv_solid); }, { ent.sv_solid = ReadByte(); }) +// gamemode specific player survival status (independent of score and frags) +ENTCS_PROP(SURVIVAL_STATUS, true, survival_status, survival_status, ENTCS_SET_NORMAL, + { WriteShort(chan, ent.survival_status); }, + { ent.survival_status = ReadShort(); }) + #ifdef SVQC int ENTCS_PUBLICMASK = 0, ENTCS_PRIVATEMASK = 0; diff --git a/qcsrc/common/gamemodes/gamemode/_mod.inc b/qcsrc/common/gamemodes/gamemode/_mod.inc index 1ca6b1d94..12a5510c3 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.inc +++ b/qcsrc/common/gamemodes/gamemode/_mod.inc @@ -16,5 +16,6 @@ #include #include #include +#include #include #include diff --git a/qcsrc/common/gamemodes/gamemode/_mod.qh b/qcsrc/common/gamemodes/gamemode/_mod.qh index ca2cffe8b..a3208d4a6 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.qh +++ b/qcsrc/common/gamemodes/gamemode/_mod.qh @@ -16,5 +16,6 @@ #include #include #include +#include #include #include diff --git a/qcsrc/common/gamemodes/gamemode/survival/_mod.inc b/qcsrc/common/gamemodes/gamemode/survival/_mod.inc new file mode 100644 index 000000000..7121d392e --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/_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/survival/_mod.qh b/qcsrc/common/gamemodes/gamemode/survival/_mod.qh new file mode 100644 index 000000000..875e4d247 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/_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/survival/cl_survival.qc b/qcsrc/common/gamemodes/gamemode/survival/cl_survival.qc new file mode 100644 index 000000000..02d0907ab --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/cl_survival.qc @@ -0,0 +1,59 @@ +#include "cl_survival.qh" + +#include +#include + +void HUD_Mod_Survival(vector pos, vector mySize) +{ + mod_active = 1; // survival should always show the mod HUD + + int mystatus = entcs_receiver(player_localnum).survival_status; + string player_text = ""; + vector player_color = '1 1 1'; + //string player_icon = ""; + if(mystatus == SURV_STATUS_HUNTER) + { + player_text = _("Hunter"); + player_color = '1 0 0'; + //player_icon = "player_red"; + } + else if(mystatus == SURV_STATUS_PREY) + { + player_text = _("Survivor"); + player_color = '0 1 0'; + //player_icon = "player_neutral"; + } + else + { + // if the player has no valid status, don't draw anything + return; + } + + drawstring_aspect(pos, player_text, vec2(mySize.x, mySize.y), player_color, panel_fg_alpha, DRAWFLAG_NORMAL); +} + +REGISTER_MUTATOR(cl_surv, true); + +MUTATOR_HOOKFUNCTION(cl_surv, ForcePlayercolors_Skip, CBC_ORDER_LAST) +{ + if(!ISGAMETYPE(SURVIVAL)) + return false; + + entity player = M_ARGV(0, entity); + entity e = entcs_receiver(player.entnum - 1); + int surv_status = ((e) ? e.survival_status : 0); + int mystatus = entcs_receiver(player_localnum).survival_status; + + int plcolor = SURV_COLOR_PREY; // default to survivor + if((mystatus == SURV_STATUS_HUNTER || intermission || STAT(GAME_STOPPED)) && surv_status == SURV_STATUS_HUNTER) + plcolor = SURV_COLOR_HUNTER; + + player.colormap = 1024 + plcolor; + return true; +} + +MUTATOR_HOOKFUNCTION(cl_surv, 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/survival/cl_survival.qh b/qcsrc/common/gamemodes/gamemode/survival/cl_survival.qh new file mode 100644 index 000000000..057120a1f --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/cl_survival.qh @@ -0,0 +1,3 @@ +#pragma once + +void HUD_Mod_Survival(vector pos, vector mySize); diff --git a/qcsrc/common/gamemodes/gamemode/survival/survival.qc b/qcsrc/common/gamemodes/gamemode/survival/survival.qc new file mode 100644 index 000000000..1f2d14402 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/survival.qc @@ -0,0 +1 @@ +#include "survival.qh" diff --git a/qcsrc/common/gamemodes/gamemode/survival/survival.qh b/qcsrc/common/gamemodes/gamemode/survival/survival.qh new file mode 100644 index 000000000..baddfb5c2 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/survival.qh @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#ifdef CSQC +void HUD_Mod_Survival(vector pos, vector mySize); +#endif +CLASS(Survival, Gametype) + INIT(Survival) + { + this.gametype_init(this, _("Survival"), "surv", "g_survival", GAMETYPE_FLAG_USEPOINTS, "", "timelimit=20 pointlimit=12", _("Identify and eliminate all the hunters before all your allies are gone")); + } + METHOD(Survival, m_isAlwaysSupported, bool(Gametype this, int spawnpoints, float diameter)) + { + return true; + } + METHOD(Survival, m_isForcedSupported, bool(Gametype this)) + { + if(!cvar("g_survival_not_lms_maps")) + { + // if this is unset, all LMS maps support Survival 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(Survival, m_modicons, void(vector pos, vector mySize), HUD_Mod_Survival); +#endif +ENDCLASS(Survival) +REGISTER_GAMETYPE(SURVIVAL, NEW(Survival)); + +#ifdef GAMEQC +// shared state signalling the player's survival status +.int survival_status; +const int SURV_STATUS_PREY = 1; +const int SURV_STATUS_HUNTER = 2; + +// hardcoded player colors for survival +const int SURV_COLOR_PREY = 51; // green +const int SURV_COLOR_HUNTER = 68; // red +#endif diff --git a/qcsrc/common/gamemodes/gamemode/survival/sv_survival.qc b/qcsrc/common/gamemodes/gamemode/survival/sv_survival.qc new file mode 100644 index 000000000..6414686e0 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/sv_survival.qc @@ -0,0 +1,431 @@ +#include "sv_survival.qh" + +float autocvar_g_survival_hunter_count = 0.25; +float autocvar_g_survival_round_timelimit = 120; +float autocvar_g_survival_warmup = 10; +bool autocvar_g_survival_punish_teamkill = true; +bool autocvar_g_survival_reward_survival = true; + +void nades_Clear(entity player); + +void Surv_UpdateScores(bool timed_out) +{ + // give players their hard-earned kills now that the round is over + FOREACH_CLIENT(true, + { + it.totalfrags += it.survival_validkills; + if(it.survival_validkills) + GameRules_scoring_add(it, SCORE, it.survival_validkills); + it.survival_validkills = 0; + // player survived the round + if(IS_PLAYER(it) && !IS_DEAD(it)) + { + if(autocvar_g_survival_reward_survival && timed_out && it.survival_status == SURV_STATUS_PREY) + GameRules_scoring_add(it, SCORE, 1); // reward survivors who make it to the end of the round time limit + if(it.survival_status == SURV_STATUS_PREY) + GameRules_scoring_add(it, SURV_SURVIVALS, 1); + else if(it.survival_status == SURV_STATUS_HUNTER) + GameRules_scoring_add(it, SURV_HUNTS, 1); + } + }); +} + +float Surv_CheckWinner() +{ + if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0) + { + // if the match times out, survivors win too! + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_SURVIVAL_SURVIVOR_WIN); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SURVIVAL_SURVIVOR_WIN); + FOREACH_CLIENT(true, + { + if(IS_PLAYER(it)) + nades_Clear(it); + }); + + Surv_UpdateScores(true); + + allowed_to_spawn = false; + game_stopped = true; + round_handler_Init(5, autocvar_g_survival_warmup, autocvar_g_survival_round_timelimit); + return 1; + } + + int survivor_count = 0, hunter_count = 0; + FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), + { + if(it.survival_status == SURV_STATUS_PREY) + survivor_count++; + else if(it.survival_status == SURV_STATUS_HUNTER) + hunter_count++; + }); + if(survivor_count > 0 && hunter_count > 0) + { + return 0; + } + + if(hunter_count > 0) // hunters win + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_SURVIVAL_HUNTER_WIN); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SURVIVAL_HUNTER_WIN); + } + else if(survivor_count > 0) // survivors win + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_SURVIVAL_SURVIVOR_WIN); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SURVIVAL_SURVIVOR_WIN); + } + else + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED); + } + + Surv_UpdateScores(false); + + allowed_to_spawn = false; + game_stopped = true; + round_handler_Init(5, autocvar_g_survival_warmup, autocvar_g_survival_round_timelimit); + + FOREACH_CLIENT(true, + { + if(IS_PLAYER(it)) + nades_Clear(it); + }); + + return 1; +} + +void Surv_RoundStart() +{ + allowed_to_spawn = boolean(warmup_stage); + int playercount = 0; + FOREACH_CLIENT(true, + { + if(IS_PLAYER(it) && !IS_DEAD(it)) + { + ++playercount; + it.survival_status = SURV_STATUS_PREY; + } + else + it.survival_status = 0; // this is mostly a safety check; if a client manages to somehow maintain a survival status, clear it before the round starts! + it.survival_validkills = 0; + }); + int hunter_count = bound(1, ((autocvar_g_survival_hunter_count >= 1) ? autocvar_g_survival_hunter_count : floor(playercount * autocvar_g_survival_hunter_count)), playercount - 1); // 20%, but ensure at least 1 and less than total + int total_hunters = 0; + FOREACH_CLIENT_RANDOM(IS_PLAYER(it) && !IS_DEAD(it), + { + if(total_hunters >= hunter_count) + break; + total_hunters++; + it.survival_status = SURV_STATUS_HUNTER; + }); + + FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), + { + if(it.survival_status == SURV_STATUS_PREY) + Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_SURVIVAL_SURVIVOR); + else if(it.survival_status == SURV_STATUS_HUNTER) + Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_SURVIVAL_HUNTER); + }); +} + +bool Surv_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 surv_isEliminated(entity e) +{ + if(INGAME_JOINED(e) && (IS_DEAD(e) || e.frags == FRAGS_PLAYER_OUT_OF_GAME)) + return true; + if(INGAME_JOINING(e)) + return true; + return false; +} + +void surv_Initialize() // run at the start of a match, initiates game mode +{ + GameRules_scoring(0, SFL_SORT_PRIO_PRIMARY, 0, { + field(SP_SURV_SURVIVALS, "survivals", 0); + field(SP_SURV_HUNTS, "hunts", SFL_SORT_PRIO_SECONDARY); + }); + + allowed_to_spawn = true; + round_handler_Spawn(Surv_CheckPlayers, Surv_CheckWinner, Surv_RoundStart); + round_handler_Init(5, autocvar_g_survival_warmup, autocvar_g_survival_round_timelimit); + EliminatedPlayers_Init(surv_isEliminated); +} + + +// ============== +// Hook Functions +// ============== + +MUTATOR_HOOKFUNCTION(surv, ClientObituary) +{ + // in survival, announcing a frag would tell everyone who the hunter 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.survival_status == frag_target.survival_status) + GiveFrags(frag_attacker, frag_target, ((autocvar_g_survival_punish_teamkill) ? -1 : -2), frag_deathtype, wep_ent.weaponentity_fld); + } + + if(frag_attacker.survival_status == SURV_STATUS_HUNTER) + M_ARGV(5, bool) = true; // anonymous attacker +} + +MUTATOR_HOOKFUNCTION(surv, PlayerPreThink) +{ + entity player = M_ARGV(0, entity); + + if(IS_PLAYER(player) || INGAME_JOINED(player)) + { + // 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 = SURV_COLOR_PREY; + if(player.survival_status == SURV_STATUS_HUNTER && game_stopped) + plcolor = SURV_COLOR_HUNTER; + setcolor(player, plcolor); + } +} + +MUTATOR_HOOKFUNCTION(surv, PlayerSpawn) +{ + entity player = M_ARGV(0, entity); + + player.survival_status = 0; + player.survival_validkills = 0; + INGAME_STATUS_SET(player, INGAME_STATUS_JOINED); + if (!warmup_stage) + eliminatedPlayers.SendFlags |= 1; +} + +MUTATOR_HOOKFUNCTION(surv, 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 && INGAME(player)) + return true; + return false; +} + +MUTATOR_HOOKFUNCTION(surv, 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 && !INGAME(player)) // not when connecting + { + INGAME_STATUS_SET(player, INGAME_STATUS_JOINING); + Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_JOIN_LATE); + } + } +} + +MUTATOR_HOOKFUNCTION(surv, reset_map_players) +{ + FOREACH_CLIENT(true, { + CS(it).killcount = 0; + it.survival_status = 0; + if (INGAME(it) || IS_BOT_CLIENT(it)) + { + TRANSMUTE(Player, it); + INGAME_STATUS_SET(it, INGAME_STATUS_JOINED); + PutClientInServer(it); + } + }); + bot_relinkplayerlist(); + return true; +} + +MUTATOR_HOOKFUNCTION(surv, reset_map_global) +{ + allowed_to_spawn = true; + return true; +} + +entity surv_LastPlayerForTeam(entity this) +{ + entity last_pl = NULL; + FOREACH_CLIENT(IS_PLAYER(it) && it != this, { + if (!IS_DEAD(it) && this.survival_status == it.survival_status) + { + if (!last_pl) + last_pl = it; + else + return NULL; + } + }); + return last_pl; +} + +void surv_LastPlayerForTeam_Notify(entity this) +{ + if (!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted()) + { + entity pl = surv_LastPlayerForTeam(this); + if (pl) + Send_Notification(NOTIF_ONE_ONLY, pl, MSG_CENTER, CENTER_ALONE); + } +} + +MUTATOR_HOOKFUNCTION(surv, PlayerDies) +{ + entity frag_attacker = M_ARGV(1, entity); + entity frag_target = M_ARGV(2, entity); + float frag_deathtype = M_ARGV(3, float); + + surv_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_clearqueue(frag_target); + } + + // killed an ally! punishment is death + if(autocvar_g_survival_punish_teamkill && frag_attacker != frag_target && IS_PLAYER(frag_attacker) && IS_PLAYER(frag_target) && frag_attacker.survival_status == frag_target.survival_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(surv, ClientDisconnect) +{ + entity player = M_ARGV(0, entity); + + if (IS_PLAYER(player) && !IS_DEAD(player)) + surv_LastPlayerForTeam_Notify(player); + return true; +} + +MUTATOR_HOOKFUNCTION(surv, MakePlayerObserver) +{ + entity player = M_ARGV(0, entity); + bool is_forced = M_ARGV(1, bool); + if (is_forced && INGAME(player)) + INGAME_STATUS_CLEAR(player); + + if (IS_PLAYER(player) && !IS_DEAD(player)) + surv_LastPlayerForTeam_Notify(player); + if (player.killindicator_teamchange == -2) // player wants to spectate + INGAME_STATUS_CLEAR(player); + if (INGAME(player)) + player.frags = FRAGS_PLAYER_OUT_OF_GAME; + if (!warmup_stage) + eliminatedPlayers.SendFlags |= 1; + if (!INGAME(player)) + { + player.survival_validkills = 0; + player.survival_status = 0; + return false; // allow team reset + } + return true; // prevent team reset +} + +MUTATOR_HOOKFUNCTION(surv, Scores_CountFragsRemaining) +{ + // announce remaining frags? + return true; +} + +MUTATOR_HOOKFUNCTION(surv, GiveFragsForKill, CBC_ORDER_FIRST) +{ + entity frag_attacker = M_ARGV(0, entity); + if(!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted()) + frag_attacker.survival_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(surv, 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 hunter! +} + +MUTATOR_HOOKFUNCTION(surv, CalculateRespawnTime) +{ + // no respawn calculations needed, player is forced to spectate anyway + return true; +} + +MUTATOR_HOOKFUNCTION(surv, Bot_FixCount, CBC_ORDER_EXCLUSIVE) +{ + FOREACH_CLIENT(IS_REAL_CLIENT(it), { + if (IS_PLAYER(it) || INGAME_JOINED(it)) + ++M_ARGV(0, int); + ++M_ARGV(1, int); + }); + return true; +} + +MUTATOR_HOOKFUNCTION(surv, ClientCommand_Spectate) +{ + entity player = M_ARGV(0, entity); + + if (INGAME(player)) + { + // 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(surv, BotShouldAttack) +{ + entity bot = M_ARGV(0, entity); + entity targ = M_ARGV(1, entity); + + if(targ.survival_status == bot.survival_status) + return true; +} diff --git a/qcsrc/common/gamemodes/gamemode/survival/sv_survival.qh b/qcsrc/common/gamemodes/gamemode/survival/sv_survival.qh new file mode 100644 index 000000000..129410a5a --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/sv_survival.qh @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +void surv_Initialize(); + +REGISTER_MUTATOR(surv, false) +{ + MUTATOR_STATIC(); + MUTATOR_ONADD + { + surv_Initialize(); + } + return false; +} + +.int survival_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/notifications/all.inc b/qcsrc/common/notifications/all.inc index ffb3f9f4b..51db0c100 100644 --- a/qcsrc/common/notifications/all.inc +++ b/qcsrc/common/notifications/all.inc @@ -443,6 +443,9 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_INFO_NOTIF(SUPERWEAPON_PICKUP, N_CONSOLE, 1, 0, "s1", "s1", "superweapons", _("^BG%s^K1 picked up a Superweapon"), "") + 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(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"), "") @@ -776,6 +779,11 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_CENTER_NOTIF(SUPERWEAPON_LOST, N_ENABLE, 0, 0, "", CPID_POWERUP, "0 0", _("^F2Superweapons have been lost"), "") MSG_CENTER_NOTIF(SUPERWEAPON_PICKUP, N_ENABLE, 0, 0, "", CPID_POWERUP, "0 0", _("^F2You now have a superweapon"), "") + MSG_CENTER_NOTIF(SURVIVAL_HUNTER, N_ENABLE, 0, 0, "", CPID_SURVIVAL, "5 0", strcat(BOLD_OPERATOR, _("^BGYou are a ^K1hunter^BG! Eliminate the survivor(s) without raising suspicion!")), "") + MSG_CENTER_NOTIF(SURVIVAL_HUNTER_WIN, N_ENABLE, 0, 0, "", CPID_ROUND, "0 0", _("^K1Hunters^BG win the round"), "") + MSG_CENTER_NOTIF(SURVIVAL_SURVIVOR, N_ENABLE, 0, 0, "", CPID_SURVIVAL, "5 0", strcat(BOLD_OPERATOR, _("^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"), "") + 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/common/notifications/all.qh b/qcsrc/common/notifications/all.qh index 9d39c2199..00bd94bb7 100644 --- a/qcsrc/common/notifications/all.qh +++ b/qcsrc/common/notifications/all.qh @@ -68,6 +68,7 @@ ENUMCLASS(CPID) CASE(CPID, OVERTIME) CASE(CPID, POWERUP) CASE(CPID, RACE_FINISHLAP) + CASE(CPID, SURVIVAL) CASE(CPID, TEAMCHANGE) CASE(CPID, TIMEOUT) CASE(CPID, TIMEIN) diff --git a/qcsrc/common/scores.qh b/qcsrc/common/scores.qh index 3449f2b53..18a74c1bf 100644 --- a/qcsrc/common/scores.qh +++ b/qcsrc/common/scores.qh @@ -60,6 +60,9 @@ REGISTER_SP(NEXBALL_FAULTS); REGISTER_SP(ONS_CAPS); REGISTER_SP(ONS_TAKES); +REGISTER_SP(SURV_SURVIVALS); +REGISTER_SP(SURV_HUNTS); + REGISTER_SP(SCORE); REGISTER_SP(KILLS); REGISTER_SP(DEATHS); diff --git a/qcsrc/menu/xonotic/util.qc b/qcsrc/menu/xonotic/util.qc index 86b0f5e87..233ce1414 100644 --- a/qcsrc/menu/xonotic/util.qc +++ b/qcsrc/menu/xonotic/util.qc @@ -662,6 +662,7 @@ float updateCompression() GAMETYPE(MAPINFO_TYPE_NEXBALL) \ GAMETYPE(MAPINFO_TYPE_ONSLAUGHT) \ GAMETYPE(MAPINFO_TYPE_ASSAULT) \ + GAMETYPE(MAPINFO_TYPE_SURVIVAL) \ /* GAMETYPE(MAPINFO_TYPE_DUEL) */ \ /**/