From: Mario Date: Thu, 17 Nov 2022 08:18:36 +0000 (+1000) Subject: Merge branch 'master' into Mario/survival X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=61fe092d7042eb1bf8aa8b4e35a807775ae7803a;p=xonotic%2Fxonotic-data.pk3dir.git Merge branch 'master' into Mario/survival --- 61fe092d7042eb1bf8aa8b4e35a807775ae7803a diff --cc qcsrc/common/gamemodes/gamemode/survival/sv_survival.qc index 4089ad597,000000000..662d2da1c mode 100644,000000..100644 --- a/qcsrc/common/gamemodes/gamemode/survival/sv_survival.qc +++ b/qcsrc/common/gamemodes/gamemode/survival/sv_survival.qc @@@ -1,430 -1,0 +1,431 @@@ +#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 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 == SV_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 == SV_STATUS_PREY) + GameRules_scoring_add(it, SV_SURVIVALS, 1); + else if(it.survival_status == SV_STATUS_HUNTER) + GameRules_scoring_add(it, SV_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 == SV_STATUS_PREY) + survivor_count++; + else if(it.survival_status == SV_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 = SV_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 = SV_STATUS_HUNTER; + }); + + FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), + { + if(it.survival_status == SV_STATUS_PREY) + Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_SURVIVAL_SURVIVOR); + else if(it.survival_status == SV_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_SV_SURVIVALS, "survivals", 0); + field(SP_SV_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(sv, 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); + } + - M_ARGV(5, bool) = true; // anonymous attacker ++ if(frag_attacker.survival_status == SV_STATUS_HUNTER) ++ M_ARGV(5, bool) = true; // anonymous attacker +} + +MUTATOR_HOOKFUNCTION(sv, 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 = SV_COLOR_PREY; + if(player.survival_status == SV_STATUS_HUNTER && game_stopped) + plcolor = SV_COLOR_HUNTER; + setcolor(player, plcolor); + } +} + +MUTATOR_HOOKFUNCTION(sv, 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(sv, 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(sv, 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(sv, 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(sv, 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(sv, 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(sv, ClientDisconnect) +{ + entity player = M_ARGV(0, entity); + + if (IS_PLAYER(player) && !IS_DEAD(player)) + surv_LastPlayerForTeam_Notify(player); + return true; +} + +MUTATOR_HOOKFUNCTION(sv, 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(sv, Scores_CountFragsRemaining) +{ + // announce remaining frags? + return true; +} + +MUTATOR_HOOKFUNCTION(sv, 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(sv, 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(sv, CalculateRespawnTime) +{ + // no respawn calculations needed, player is forced to spectate anyway + return true; +} + +MUTATOR_HOOKFUNCTION(sv, 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(sv, 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(sv, BotShouldAttack) +{ + entity bot = M_ARGV(0, entity); + entity targ = M_ARGV(1, entity); + + if(targ.survival_status == bot.survival_status) + return true; +} diff --cc qcsrc/common/scores.qh index 9e570e30e,3449f2b53..805a022a7 --- a/qcsrc/common/scores.qh +++ b/qcsrc/common/scores.qh @@@ -85,11 -57,31 +57,34 @@@ REGISTER_SP(LMS_LIVES) REGISTER_SP(NEXBALL_GOALS); REGISTER_SP(NEXBALL_FAULTS); - REGISTER_SP(ONS_TAKES); REGISTER_SP(ONS_CAPS); + REGISTER_SP(ONS_TAKES); +REGISTER_SP(SV_SURVIVALS); +REGISTER_SP(SV_HUNTS); ++ + REGISTER_SP(SCORE); + REGISTER_SP(KILLS); + REGISTER_SP(DEATHS); + REGISTER_SP(TEAMKILLS); + REGISTER_SP(SUICIDES); + REGISTER_SP(DMG); + REGISTER_SP(DMGTAKEN); + + REGISTER_SP(ELO); // not sortable + REGISTER_SP(FPS); // not sortable + + // fields not networked via the score system + REGISTER_SP(END); + + REGISTER_SP(PING); + REGISTER_SP(PL); + REGISTER_SP(NAME); + REGISTER_SP(SEPARATOR); + + REGISTER_SP(KDRATIO); // kills / deaths + REGISTER_SP(SUM); // kills - deaths + REGISTER_SP(FRAGS); // kills - suicides #endif