From: Mario Date: Sun, 12 Jul 2020 14:48:21 +0000 (+1000) Subject: Survival gamemode: Identify and eliminate all the hunters before all your allies... X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=8f61a40877542ac94baa74c5ed653c77b77bd855;p=xonotic%2Fxonotic-data.pk3dir.git Survival gamemode: Identify and eliminate all the hunters before all your allies are gone --- diff --git a/gamemodes-client.cfg b/gamemodes-client.cfg index c43b9d1d3..a95ac6d66 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_surv 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 b1631b233..94ace93e2 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_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 @@ -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_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. @@ -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_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 // ========= @@ -551,3 +560,14 @@ 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" + +// ========== +// survival +// ========== +set g_survival 0 "Survival: identify and eliminate all the hunters before all your allies are gone" +set g_survival_not_dm_maps 0 "when this is set, DM 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 180 "round time limit in seconds" diff --git a/gfx/menu/luma/gametype_surv.tga b/gfx/menu/luma/gametype_surv.tga new file mode 100644 index 000000000..e89e60b49 Binary files /dev/null and b/gfx/menu/luma/gametype_surv.tga differ diff --git a/gfx/menu/luminos/gametype_surv.tga b/gfx/menu/luminos/gametype_surv.tga new file mode 100644 index 000000000..67a545d70 Binary files /dev/null and b/gfx/menu/luminos/gametype_surv.tga differ diff --git a/gfx/menu/wickedx/gametype_surv.tga b/gfx/menu/wickedx/gametype_surv.tga new file mode 100644 index 000000000..67a545d70 Binary files /dev/null and b/gfx/menu/wickedx/gametype_surv.tga differ diff --git a/gfx/menu/xaw/gametype_surv.tga b/gfx/menu/xaw/gametype_surv.tga new file mode 100644 index 000000000..bc72cd107 Binary files /dev/null and b/gfx/menu/xaw/gametype_surv.tga differ diff --git a/notifications.cfg b/notifications.cfg index 41f0706ea..814dccc9f 100644 --- a/notifications.cfg +++ b/notifications.cfg @@ -282,6 +282,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_SUVIVOR_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)" @@ -532,6 +534,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/client/csqcmodel_hooks.qc b/qcsrc/client/csqcmodel_hooks.qc index 99456bd3e..f08378b6a 100644 --- a/qcsrc/client/csqcmodel_hooks.qc +++ b/qcsrc/client/csqcmodel_hooks.qc @@ -133,6 +133,9 @@ void CSQCPlayer_ModelAppearance_PostUpdate(entity this) } void CSQCPlayer_ModelAppearance_Apply(entity this, bool islocalplayer) { + int cm = this.forceplayermodels_savecolormap; + cm = (cm >= 1024) ? cm : (entcs_GetClientColors(cm - 1) + 1024); + if(MUTATOR_CALLHOOK(ForcePlayermodels_Skip, this, islocalplayer)) goto skipforcemodels; @@ -193,9 +196,6 @@ void CSQCPlayer_ModelAppearance_Apply(entity this, bool islocalplayer) // apply it bool isfriend; - int cm; - cm = this.forceplayermodels_savecolormap; - cm = (cm >= 1024) ? cm : (entcs_GetClientColors(cm - 1) + 1024); if(teamplay) isfriend = (cm == 1024 + 17 * myteam); @@ -227,6 +227,11 @@ void CSQCPlayer_ModelAppearance_Apply(entity this, bool islocalplayer) this.skin = this.forceplayermodels_saveskin; } + LABEL(skipforcemodels) + + if(MUTATOR_CALLHOOK(ForcePlayercolors_Skip, this, islocalplayer)) + goto skipforcecolors; + // forceplayercolors too if(teamplay) { @@ -280,7 +285,7 @@ void CSQCPlayer_ModelAppearance_Apply(entity this, bool islocalplayer) this.colormap = player_localnum + 1; } - LABEL(skipforcemodels) + LABEL(skipforcecolors) if((this.csqcmodel_effects & CSQCMODEL_EF_RESPAWNGHOST) && !autocvar_cl_respawn_ghosts_keepcolors) { diff --git a/qcsrc/client/hud/panel/scoreboard.qc b/qcsrc/client/hud/panel/scoreboard.qc index 120feeafa..999968020 100644 --- a/qcsrc/client/hud/panel/scoreboard.qc +++ b/qcsrc/client/hud/panel/scoreboard.qc @@ -364,13 +364,13 @@ void Cmd_Scoreboard_Help() // e.g. -teams,rc,cts,lms/kills ?+rc/kills #define SCOREBOARD_DEFAULT_COLUMNS \ "ping pl fps name |" \ -" -teams,rc,cts,inv,lms/kills +ft,tdm/kills ?+rc,inv/kills" \ -" -teams,lms/deaths +ft,tdm/deaths" \ +" -teams,rc,cts,inv,lms,surv/kills +ft,tdm/kills ?+rc,inv/kills" \ +" -teams,lms,surv/deaths +ft,tdm/deaths" \ " +tdm/sum" \ -" -teams,lms,rc,cts,inv,ka/suicides +ft,tdm/suicides ?+rc,inv/suicides" \ -" -cts,dm,tdm,ka,ft/frags" /* tdm already has this in "score" */ \ +" -teams,lms,rc,cts,inv,ka,surv/suicides +ft,tdm/suicides ?+rc,inv/suicides" \ +" -cts,dm,tdm,ka,ft,surv/frags" /* tdm already has this in "score" */ \ " +tdm,ft,dom,ons,as/teamkills"\ -" -rc,cts,nb/dmg -rc,cts,nb/dmgtaken" \ +" -rc,cts,nb,surv/dmg -rc,cts,nb,surv/dmgtaken" \ " +ctf/pickups +ctf/fckills +ctf/returns +ctf/caps +ons/takes +ons/caps" \ " +lms/lives +lms/rank" \ " +kh/kckills +kh/losses +kh/caps" \ diff --git a/qcsrc/client/mutators/events.qh b/qcsrc/client/mutators/events.qh index 0629c2a9f..2b1792878 100644 --- a/qcsrc/client/mutators/events.qh +++ b/qcsrc/client/mutators/events.qh @@ -179,13 +179,20 @@ MUTATOR_HOOKABLE(DrawViewModel, EV_DrawViewModel); /** Called when updating the view's liquid contents, return true to disable the standard checks and apply your own */ MUTATOR_HOOKABLE(HUD_Contents, EV_NO_ARGS); -/** Return true to disable player model/color forcing */ +/** Return true to disable player model forcing */ #define EV_ForcePlayermodels_Skip(i, o) \ /** entity id */ i(entity, MUTATOR_ARGV_0_entity) \ /** is local */ i(bool, MUTATOR_ARGV_1_bool) \ /**/ MUTATOR_HOOKABLE(ForcePlayermodels_Skip, EV_ForcePlayermodels_Skip); +/** Return true to disable player color forcing */ +#define EV_ForcePlayercolors_Skip(i, o) \ + /** entity id */ i(entity, MUTATOR_ARGV_0_entity) \ + /** is local */ i(bool, MUTATOR_ARGV_1_bool) \ + /**/ +MUTATOR_HOOKABLE(ForcePlayercolors_Skip, EV_ForcePlayercolors_Skip); + /** Called when damage info is received on the client, useful for playing explosion effects */ #define EV_DamageInfo(i, o) \ /** entity id */ i(entity, MUTATOR_ARGV_0_entity) \ diff --git a/qcsrc/common/ent_cs.qc b/qcsrc/common/ent_cs.qc index a7aa27961..abd7c2c35 100644 --- a/qcsrc/common/ent_cs.qc +++ b/qcsrc/common/ent_cs.qc @@ -152,6 +152,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; diff --git a/qcsrc/common/gamemodes/gamemode/_mod.inc b/qcsrc/common/gamemodes/gamemode/_mod.inc index a33ec87a0..e261fa7b9 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.inc +++ b/qcsrc/common/gamemodes/gamemode/_mod.inc @@ -15,4 +15,5 @@ #include #include #include +#include #include diff --git a/qcsrc/common/gamemodes/gamemode/_mod.qh b/qcsrc/common/gamemodes/gamemode/_mod.qh index ffd71d59d..928bd44cb 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.qh +++ b/qcsrc/common/gamemodes/gamemode/_mod.qh @@ -15,4 +15,5 @@ #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..a7899dadb --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/cl_survival.qc @@ -0,0 +1,40 @@ +#include "cl_survival.qh" + +#include + +void HUD_Mod_Survival(vector pos, vector mySize) +{ + mod_active = 1; // survival should always show the mod HUD + + // since survivor state is the default value, spectators are considered survivors + // so we must hide them manually here + if(entcs_GetSpecState(player_localnum) == ENTCS_SPEC_PURE) + return; // no icon while spectating + + int mystatus = entcs_receiver(player_localnum).survival_status; + //string player_icon = ((mystatus == SURV_STATUS_HUNTER) ? "player_red" : "player_neutral"); + string player_text = ((mystatus == SURV_STATUS_HUNTER) ? _("Hunter") : _("Survivor")); + vector player_color = ((mystatus == SURV_STATUS_HUNTER) ? '1 0 0' : '0 1 0'); + //drawpic_aspect_skin(pos, player_icon, vec2(0.5 * mySize.x, mySize.y), '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL); + 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) +{ + 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; // green + if((mystatus == SURV_STATUS_HUNTER || intermission || STAT(GAME_STOPPED)) && surv_status == SURV_STATUS_HUNTER) + plcolor = SURV_COLOR_HUNTER; // red + + player.colormap = 1024 + plcolor; + return true; +} 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..6f70f09be --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/cl_survival.qh @@ -0,0 +1 @@ +#pragma once 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..314ec629e --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/survival.qh @@ -0,0 +1,10 @@ +#pragma once + +// shared state signalling the player's survival status +.int survival_status; +const int SURV_STATUS_PREY = 0; +const int SURV_STATUS_HUNTER = 1; + +// hardcoded player colors for survival +const int SURV_COLOR_PREY = 51; // green +const int SURV_COLOR_HUNTER = 68; // red 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..72a0c499e --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/sv_survival.qc @@ -0,0 +1,420 @@ +#include "sv_survival.qh" + +float autocvar_g_survival_hunter_count = 0.25; +float autocvar_g_survival_round_timelimit = 180; +float autocvar_g_survival_warmup = 10; +bool autocvar_g_survival_punish_teamkill = true; +bool autocvar_g_survival_reward_survival = true; + +void surv_FakeTimeLimit(entity e, float t) +{ + if(!IS_REAL_CLIENT(e)) + return; + 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); +} + +void Surv_count_alive_players() +{ + total_players = 0; + FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), + { + ++total_players; + }); +} + +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; + if(autocvar_g_survival_reward_survival && timed_out && IS_PLAYER(it) && !IS_DEAD(it) && 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 + }); +} + +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_FakeTimeLimit(it, -1); + }); + + 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); + surv_FakeTimeLimit(it, -1); + }); + + return 1; +} + +void Surv_RoundStart() +{ + allowed_to_spawn = boolean(warmup_stage); + FOREACH_CLIENT(true, { it.survival_status = SURV_STATUS_PREY; it.survival_validkills = 0; }); + Surv_count_alive_players(); + int hunter_count = bound(1, ((autocvar_g_survival_hunter_count >= 1) ? autocvar_g_survival_hunter_count : floor(total_players * autocvar_g_survival_hunter_count)), total_players - 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); + + surv_FakeTimeLimit(it, round_handler_GetEndTime()); + }); +} + +bool Surv_CheckPlayers() +{ + static int prev_missing_players; + allowed_to_spawn = true; + Surv_count_alive_players(); + if (total_players >= 2) + { + if(prev_missing_players > 0) + Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS); + prev_missing_players = -1; + return true; + } + if(total_players == 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(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 surv_Initialize() // run at the start of a match, initiates game mode +{ + 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 + // for the sake of anonymity, a barebones + 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); + } + + M_ARGV(5, bool) = true; // anonymous attacker +} + +MUTATOR_HOOKFUNCTION(surv, PlayerSpawn) +{ + entity player = M_ARGV(0, entity); + + player.survival_status = SURV_STATUS_PREY; + player.caplayer = 1; + 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 && player.caplayer) + 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 && !player.caplayer) // not when connecting + { + player.caplayer = 0.5; + 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; + 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(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, 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_clear(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 + ClientKill_Silent(frag_attacker, 0); + //Damage(frag_attacker, frag_attacker, frag_attacker, 100000, DEATH_AUTOTEAMCHANGE.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); + + if (IS_PLAYER(player) && !IS_DEAD(player)) + surv_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.survival_validkills = 0; + player.survival_status = 0; + surv_FakeTimeLimit(player, -1); // restore original timelimit + 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); + 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) || it.caplayer == 1) + ++M_ARGV(0, int); + ++M_ARGV(1, int); + }); + return true; +} + +MUTATOR_HOOKFUNCTION(surv, 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(surv, GetPlayerStatus) +{ + entity player = M_ARGV(0, entity); + + return player.caplayer == 1; +} + +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..ad68f02bf --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/survival/sv_survival.qh @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +void surv_Initialize(); + +REGISTER_MUTATOR(surv, false) +{ + MUTATOR_STATIC(); + MUTATOR_ONADD + { + GameRules_scoring(0, SFL_SORT_PRIO_PRIMARY, 0, { + }); + + 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/mapinfo.qh b/qcsrc/common/mapinfo.qh index 9b4dfa257..c1fff6350 100644 --- a/qcsrc/common/mapinfo.qh +++ b/qcsrc/common/mapinfo.qh @@ -608,6 +608,34 @@ ENDCLASS(Duel) REGISTER_GAMETYPE(DUEL, NEW(Duel)); #define g_duel IS_GAMETYPE(DUEL) +#ifdef CSQC +void HUD_Mod_Survival(vector pos, vector mySize); +#endif +CLASS(Survival, Gametype) + INIT(Survival) + { + this.gametype_init(this, _("Survival"),"surv","g_survival",false,true,"","timelimit=20 pointlimit=20",_("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_duel_not_dm_maps")) + { + // if this is set, all DM maps support Survival too + if(!(MapInfo_Map_supportedGametypes & this.m_flags) && (MapInfo_Map_supportedGametypes & MAPINFO_TYPE_DEATHMATCH.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)); + const int MAPINFO_FEATURE_WEAPONS = 1; // not defined for instagib-only maps const int MAPINFO_FEATURE_VEHICLES = 2; const int MAPINFO_FEATURE_TURRETS = 4; diff --git a/qcsrc/common/notifications/all.inc b/qcsrc/common/notifications/all.inc index a9a2ee655..296b31835 100644 --- a/qcsrc/common/notifications/all.inc +++ b/qcsrc/common/notifications/all.inc @@ -435,6 +435,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"), "") @@ -763,6 +766,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 481154f3e..efe602a47 100644 --- a/qcsrc/common/notifications/all.qh +++ b/qcsrc/common/notifications/all.qh @@ -73,6 +73,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/menu/xonotic/util.qc b/qcsrc/menu/xonotic/util.qc index c0a8c6b2b..236f5ae07 100644 --- a/qcsrc/menu/xonotic/util.qc +++ b/qcsrc/menu/xonotic/util.qc @@ -683,6 +683,7 @@ float updateCompression() GAMETYPE(MAPINFO_TYPE_ASSAULT) \ /* GAMETYPE(MAPINFO_TYPE_DUEL) */ \ /* GAMETYPE(MAPINFO_TYPE_INVASION) */ \ + /* GAMETYPE(MAPINFO_TYPE_SURVIVAL) */ \ /**/ // hidden gametypes come last so indexing always works correctly diff --git a/qcsrc/server/g_damage.qc b/qcsrc/server/g_damage.qc index 274378f17..9a8be98da 100644 --- a/qcsrc/server/g_damage.qc +++ b/qcsrc/server/g_damage.qc @@ -209,12 +209,12 @@ float Obituary_WeaponDeath( return true; } -bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target) +bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target, string attacker_name) { if(deathtype == DEATH_FIRE.m_id) { Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)); - Send_Notification(NOTIF_ONE, targ, MSG_CHOICE, CHOICE_FRAGGED_FIRE, attacker.netname, kill_count_to_target, GetResource(attacker, RES_HEALTH), GetResource(attacker, RES_ARMOR), (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)); + Send_Notification(NOTIF_ONE, targ, MSG_CHOICE, CHOICE_FRAGGED_FIRE, attacker_name, kill_count_to_target, GetResource(attacker, RES_HEALTH), GetResource(attacker, RES_ARMOR), (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)); return true; } @@ -229,16 +229,25 @@ void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .en // Declarations float notif_firstblood = false; float kill_count_to_attacker, kill_count_to_target; + bool notif_anonymous = false; + string attacker_name = attacker.netname; // Set final information for the death targ.death_origin = targ.origin; string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : ""); + // Abort now if a mutator requests it + if (MUTATOR_CALLHOOK(ClientObituary, inflictor, attacker, targ, deathtype, attacker.(weaponentity))) { CS(targ).killcount = 0; return; } + notif_anonymous = M_ARGV(5, bool); + + if(notif_anonymous) + attacker_name = "Anonymous player"; + #ifdef NOTIFICATIONS_DEBUG Debug_Notification( sprintf( "Obituary(%s, %s, %s, %s = %d);\n", - attacker.netname, + attacker_name, inflictor.netname, targ.netname, Deathtype_Name(deathtype), @@ -299,8 +308,8 @@ void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .en CS(attacker).killcount = 0; Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname); - Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker.netname); - Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker.netname, deathlocation, CS(targ).killcount); + Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker_name); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker_name, deathlocation, CS(targ).killcount); // In this case, the death message will ALWAYS be "foo was betrayed by bar" // No need for specific death/weapon messages... @@ -364,14 +373,14 @@ void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .en targ, MSG_CHOICE, CHOICE_TYPEFRAGGED, - attacker.netname, + attacker_name, kill_count_to_target, GetResource(attacker, RES_HEALTH), GetResource(attacker, RES_ARMOR), (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping) ); } - else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target)) + else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target, attacker_name)) { Send_Notification( NOTIF_ONE, @@ -387,7 +396,7 @@ void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .en targ, MSG_CHOICE, CHOICE_FRAGGED, - attacker.netname, + attacker_name, kill_count_to_target, GetResource(attacker, RES_HEALTH), GetResource(attacker, RES_ARMOR), @@ -399,8 +408,8 @@ void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .en if(deathtype == DEATH_BUFF.m_id) f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id; - if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker)) - Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3); + if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker)) + Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3); } } diff --git a/qcsrc/server/g_world.qc b/qcsrc/server/g_world.qc index 7231bd8f6..0ea592141 100644 --- a/qcsrc/server/g_world.qc +++ b/qcsrc/server/g_world.qc @@ -287,6 +287,8 @@ void cvar_changes_init() BADCVAR("g_runematch"); BADCVAR("g_shootfromeye"); BADCVAR("g_snafu"); + BADCVAR("g_survival"); + BADCVAR("g_survival_not_dm_maps"); BADCVAR("g_tdm"); BADCVAR("g_tdm_on_dm_maps"); BADCVAR("g_tdm_teams"); diff --git a/qcsrc/server/mutators/events.qh b/qcsrc/server/mutators/events.qh index a310c6cca..35a8f6fd9 100644 --- a/qcsrc/server/mutators/events.qh +++ b/qcsrc/server/mutators/events.qh @@ -90,6 +90,17 @@ MUTATOR_HOOKABLE(PlayerDies, EV_PlayerDies); /**/ MUTATOR_HOOKABLE(PlayerDied, EV_PlayerDied); +/** called when showing an obituary for the player. return true to show nothing (workarounds may be needed) */ +#define EV_ClientObituary(i, o) \ + /** inflictor */ i(entity, MUTATOR_ARGV_0_entity) \ + /** attacker */ i(entity, MUTATOR_ARGV_1_entity) \ + /** target */ i(entity, MUTATOR_ARGV_2_entity) \ + /** deathtype */ i(float, MUTATOR_ARGV_3_float) \ + /** wep entity */ i(entity, MUTATOR_ARGV_4_entity) \ + /** anonymous killer*/ o(bool, MUTATOR_ARGV_5_bool) \ + /**/ +MUTATOR_HOOKABLE(ClientObituary, EV_ClientObituary); + /** allows overriding the frag centerprint messages */ #define EV_FragCenterMessage(i, o) \ /** attacker */ i(entity, MUTATOR_ARGV_0_entity) \